11. Ejercicio práctico – version 4
La aplicación de cálculo de impuestos implementará la siguiente estructura por capas:

Vamos a retomar los elementos de version 3 del párrafo anterior y modificarlos para adaptarlos a la nueva arquitectura de la aplicación. A esto se le denomina a veces «refactorización». Suponemos aquí que los datos necesarios para la aplicación se encuentran en archivos de texto. La capa [Dao] se encargará de los intercambios con estos archivos.
11.1. Estructura de los scripts

11.2. Objetos intercambiados entre capas
Vamos a conservar algunos objetos de la capa version 3. Los volvemos a incluir aquí a modo de recordatorio.
La excepción [ExceptionImpots] es la excepción que lanzará la capa [Dao] cuando encuentre un problema, ya sea con el acceso a los datos o con la naturaleza de los datos (datos incorrectos).
<?php
// espacio de nombres
namespace Application;
class ExceptionImpots extends \RuntimeException {
public function __construct(string $message, int $code=0) {
parent::__construct($message, $code);
}
}
La clase [Utilitaires] reúne métodos útiles para la gestión de archivos de texto (en este caso, un único método):
<?php
// espacio de nombres
namespace Application;
// una clase de funciones de utilidad
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
// se elimina el marcador de fin de línea de $ligne si existe
$longueur = strlen($ligne); // longitud de línea
while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
$ligne = substr($ligne, 0, $longueur - 1);
$longueur--;
}
// fin: se devuelve la línea
return($ligne);
}
}
La clase [TaxAdminData] es la clase que encapsula los datos de la administración tributaria:
<?php
namespace Application;
class TaxAdminData {
// tramos impositivos
private $limites;
private $coeffR;
private $coeffN;
// constantes de cálculo del impuesto
private $plafondQfDemiPart;
private $plafondRevenusCelibatairePourReduction;
private $plafondRevenusCouplePourReduction;
private $valeurReducDemiPart;
private $plafondDecoteCelibataire;
private $plafondDecoteCouple;
private $plafondImpotCouplePourDecote;
private $plafondImpotCelibatairePourDecote;
private $abattementDixPourcentMax;
private $abattementDixPourcentMin;
// inicialización
public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
// se recupera el contenido del archivo de datos fiscales
$fileContents = \file_get_contents($taxAdminDataFilename);
…
// se devuelve el objeto
return $this;
}
private function check($value): \stdClass {
…
return $result;
}
// toString
public function __toString() {
// cadena Json del objeto
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
// getters y setters
public function getLimites() {
return $this->limites;
}
…
public function setLimites($limites) {
$this->limites = $limites;
return $this;
}
…
}
Añadimos una nueva clase [TaxPayerData] que encapsula los datos escritos en el archivo de resultados:
<?php
// espacio de nombres
namespace Application;
// la clase de datos
class TaxPayerData {
// datos necesarios para el cálculo del impuesto del contribuyente
private $marié;
private $enfants;
private $salaire;
// resultados del cálculo del impuesto
private $montant;
private $surcôte;
private $décôte;
private $réduction;
private $taux;
// setter
public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel) : TaxPayerData{
// datos del contribuyente necesarios para el cálculo del impuesto
$this->marié = $marié;
$this->enfants = $nbEnfants;
$this->salaire = $salaireAnnuel;
// se devuelve el objeto inicializado
return $this;
}
// getters y setters
public function getMarié() {
return $this->marié;
}
…
public function setMarié($marié) {
$this->marié = $marié;
return $this;
}
…
// toString
public function __toString() {
// cadena Json del objeto
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
}
Nota: utilice la generación automática de código para generar el constructor, los getters y los setters (véase el párrafo del enlace). Tenga en cuenta que los setters son «fluidos».
11.3. La capa [dao]
Aquí nos centramos en la capa [1] de nuestra aplicación:

11.3.1. La interfaz [InterfaceDao]
La interfaz de la capa [dao] será la siguiente [InterfaceDao.php]:
<?php
// espacio de nombres
namespace Application;
interface InterfaceDao {
// lectura de datos de contribuyentes
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// lectura de los datos de la administración tributaria (tramos impositivos)
public function getTaxAdminData(): TaxAdminData;
// registro de resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Comentarios
- Las especificaciones son las siguientes:
- los datos de los contribuyentes se encuentran en un archivo de texto;
- los resultados del cálculo de impuestos se guardan en un archivo de texto;
- los posibles errores se guardan en un archivo de texto;
- se desconoce en qué formato están disponibles los datos de la administración tributaria. Para cada nuevo formato, la interfaz [InterfaceDao] deberá implementarse mediante una nueva clase;
- los métodos de la interfaz que encuentren un error irrecuperable al acceder a los datos deben lanzar una excepción de tipo [ExceptionImpots];
- línea 9: el método que permite obtener los datos del contribuyente [statut marital, nombre d’enfants, salaire annuel];
- el primer parámetro es el nombre del archivo de texto en el que se encuentran estos datos;
- el segundo parámetro es el nombre del archivo de texto en el que se registrarán los posibles errores encontrados;
- línea 12: el método que permite obtener los datos de la administración tributaria. Aquí no se le pasa ningún parámetro porque no se sabe cómo están almacenados;
- línea 15: el método que permite guardar los resultados del cálculo del impuesto en un archivo de texto cuyo nombre se pasa como parámetro;
Al escribir la interfaz [InterfaceDao], sabemos que habrá diferentes formas de implementar el método [getTaxAdminData] en función de cómo se almacenen los datos de la administración tributaria. Por lo tanto, la interfaz [InterfaceDao] se implementará mediante diferentes clases, cada una de las cuales se encargará de un almacenamiento concreto de estos datos (tablas, archivos de texto, base de datos, servicio web). No obstante, estas clases derivadas tendrán un código común, el de la implementación de los métodos [getTaxPayersData, saveResults]. Sabemos que este caso de uso puede implementarse de dos maneras (véase el párrafo del enlace):
- se crea una clase abstracta C que agrupa el código común a las clases derivadas. La clase C implementa la interfaz I, pero algunos métodos que deben declararse en las clases derivadas se declaran abstractos en la clase C y, por lo tanto, la clase C es en sí misma abstracta. A continuación, se crean las clases C1 y C2 derivadas de C, que implementan cada una a su manera los métodos no definidos (abstractos) de su clase padre C;
- Se crea un rasgo T casi idéntico a la clase abstracta C de la solución anterior. Este rasgo no implementa la interfaz I, ya que sintácticamente no puede hacerlo. A continuación, se crean las clases C1 y C2 que implementan la interfaz I y utilizan el rasgo T. A estas clases solo les queda implementar los métodos de la interfaz I no implementados por el rasgo T que importan;
Para el ejemplo, utilizaremos aquí un rasgo [TraitDao].
11.3.2. El rasgo [TraitDao]
El código del rasgo [TraitDao] es el siguiente [TraitDao.php]:
<?php
// espacio de nombres
namespace Application;
trait TraitDao {
// lectura de datos de contribuyentes
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// tabla de datos de contribuyentes
$taxPayersData = [];
// tabla de errores
$errors = [];
// pueden producirse bastantes errores al gestionar archivos
try {
// lectura de datos de usuario
// cada línea tiene el formato: estado civil, número de hijos, salario anual
$taxPayersFile = fopen($taxPayersFilename, "r");
if (!$taxPayersFile) {
throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
}
// se procesa la línea actual del archivo de datos de usuario
// que tiene el formato estado civil, número de hijos, salario anual
$num = 1; // n.º de la línea actual
$nbErreurs = 0; // número de errores encontrados
while ($ligne = fgets($taxPayersFile, 100)) {
// se omiten las líneas vacías
$ligne = trim($ligne);
if (strlen($ligne) == 0) {
// línea siguiente
$num++;
// se vuelve al principio
continue;
}
// se elimina el posible carácter de fin de línea
$ligne = Utilitaires::cutNewLineChar($ligne);
// se recuperan los 3 campos «casado:hijos:salario» que forman $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// se comprueban
// el estado civil debe ser sí o no
$marié = trim(strtolower($marié));
$erreur = ($marié !== "oui" and $marié !== "non");
if (!$erreur) {
// el número de hijos debe ser un número entero
$enfants = trim($enfants);
if (!preg_match("/^\d+$/", $enfants)) {
$erreur = TRUE;
} else {
$enfants = (int) $enfants;
}
}
if (!$erreur) {
// el salario es un número entero sin céntimos de euro
$salaire = trim($salaire);
if (!preg_match("/^\d+$/", $salaire)) {
$erreur = TRUE;
} else {
$salaire = (int) $salaire;
}
}
//es un error?
if ($erreur) {
$errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
$nbErreurs++;
} else {
// se guarda la información
$taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
}
// línea siguiente
$num++;
}
// ¿Estamos al final del archivo?
if (!feof($taxPayersFile)) {
// se ha salido del bucle por un error de lectura
throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
} else {
// se ha salido del bucle al llegar a la marca de fin de archivo
// se guardan los errores en un archivo de texto
$this->saveString($errorsFilename, implode("\n", $errors));
// resultado de la función
return $taxPayersData;
}
} finally {
// se cierra el archivo si está abierto
if ($taxPayersFile) {
fclose($taxPayersFile);
}
}
}
// almacenamiento de los resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// almacenamiento de la tabla [$taxPayersData] en el archivo de texto [$resultsFileName]
// si el archivo de texto [$resultsFileName] no existe, se crea
$this->saveString($resultsFilename, implode("\n", $taxPayersData));
}
// grabación de los resultados de una tabla en un archivo de texto
private function saveString(string $fileName, string $data): void {
// guardar la tabla [$data] en el archivo de texto [$fileName]
// si el archivo de texto [$fileName] no existe, se crea
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
}
}
}
Comentarios
- línea 6: aquí definimos un trait y no una clase;
- líneas 9-89: el método [getTaxPayersData] implementa el método del mismo nombre de la interfaz [InterfaceDao]. Recupera en un archivo de texto denominado [$taxPayersFilename] los datos de los contribuyentes [statut marital, nombre d’enfants, salaire annuel]. Los presenta en forma de una tabla [$taxPayersData] de elementos de tipo [TaxPayerData] (líneas 67, 81);
- el método [getTaxPayersData] es muy similar al método [AbstractBaseImpots::executeBatchImpots] descrito en el apartado «Enlace», con las siguientes diferencias:
- el método [getTaxPayersData] solo recupera los datos de los contribuyentes. No realiza ningún cálculo de impuestos. Aquí es donde entra en juego la capa [métier];
- al igual que el método [executeBatchImpots], señala los errores. Aquí los errores se almacenan primero en una tabla [$errors] (línea 13), tabla que se guarda en un archivo de texto al final del procesamiento (línea 79). Según el caso, puede estar vacía o no;
- en caso de error irrecuperable, se lanza una excepción de tipo [ExceptionImpots] (líneas 20, 75);
- línea 73: cabe destacar el procesamiento realizado al salir del bucle de las líneas 26-71. De hecho, la función [fgets] tiene el inconveniente de establecer el valor booleano FALSE tanto cuando la lectura de las líneas ha encontrado la marca de fin de archivo como si dicha lectura no ha podido completarse debido a un error. Para diferenciar ambos casos, se comprueba si se ha llegado al final del archivo con la función [feof]. Si no se ha llegado al final del archivo, significa que se ha producido un error y, por lo tanto, se lanza una excepción;
- líneas 83-88: se ejecuta [finally] independientemente de si se ha producido una excepción durante el procesamiento del archivo;
- línea 85: si se ha abierto el archivo, el «handle» [$taxPayersFile] del archivo tiene el valor booleano TRUE; en caso contrario, FALSE;
- líneas 99-105: el método privado [saveString] utilizado en la línea 79 para guardar la tabla de errores en un archivo de texto;
- línea 99: el método [saveString] recibe dos parámetros:
- [string $filename], que es el nombre del archivo de texto utilizado para guardar los datos;
- [string $data], que es la cadena de caracteres que se va a guardar en el archivo de texto. Esta cadena será un conjunto de líneas terminadas por el carácter de fin de línea \n;
- línea 102: la función PHP [file_puts_contents] guarda una cadena de caracteres en un archivo de texto. Se encarga de abrir el archivo, escribir la cadena en él y cerrar el archivo. Devuelve el valor booleano FALSE si se ha producido un error;
- línea 103: si se produce un error, se lanza una excepción;
- líneas 92-96: implementación del método [saveResults] de la interfaz [InterfaceDao]. Se vuelve a utilizar el método privado [saveString]. Aquí, el segundo parámetro de [saveString] es una cadena construida a partir de la matriz [$taxPayersData], cuyos elementos son de tipo [TaxPayerData]. Cabe preguntarse cuál será el resultado de la operación:
implode("\n", $taxPayersData)
Hemos definido en la clase [TaxPayerData] (apartado enlace) el siguiente método [__toString]:
public function __toString() {
// cadena Json del objeto
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
La operación
implode("\n", $taxPayersData)
concatenará cada elemento de la matriz [$taxPayersData], transformado en cadena de caracteres mediante su método [__toString], con el carácter de fin de línea \n. Esto dará como resultado una cadena de caracteres con el siguiente formato:
json1\njson2\n…
Conclusión
El rasgo [TraitDao] ha implementado dos de los métodos de la interfaz [InterfaceDao], [getTaxPayersData] y [saveResults]:
<?php
// espacio de nombres
namespace Application;
interface InterfaceDao {
// lectura de los datos de los contribuyentes
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// lectura de los datos de la administración tributaria (tramos impositivos)
public function getTaxAdminData(): TaxAdminData;
// registro de resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Nos queda por implementar el método [getTaxAdminData], que recupera los datos de la administración tributaria.
11.3.3. La clase [ImpotsWithTaxAdminDataInJsonFile]
La clase [ImpotsWithTaxAdminDataInJsonFile] implementa la interfaz [InterfaceDao] de la siguiente manera:
<?php
// espacio de nombres
namespace Application;
// definición de una clase ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
// uso de una característica
use TraitDao;
// el objeto de tipo TaxAdminData que contiene los datos de los tramos impositivos
private $taxAdminData;
// el constructor
public function __construct(string $taxAdminDataFilename) {
// se desea inicializar el atributo [$this->taxAdminData]
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// devuelve los datos que permiten calcular el impuesto
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Comentarios
- línea 7: la clase [ImpotsWithTaxAdminDataInJsonFile] implementa la interfaz [InterfaceDao];
- línea 9: la clase [ImpotsWithTaxAdminDataInJsonFile] utiliza el rasgo [traitDao], que, como sabemos, implementa los métodos [getTaxPayersData] y [saveResults] de lainterfaz [InterfaceDao]. Por lo tanto, a la clase [ImpotsWithTaxAdminDataInJsonFile] solo le queda implementar el método [getTaxAdminData], que recupera los datos de la administración tributaria;
- línea 11: el atributo de tipo [TaxAdminData] que devuelve el método [getTaxAdminData] de las líneas 20-22. Este atributo es inicializado por el constructor de las líneas 14-17;
Hemos terminado con la capa [dao] de nuestra aplicación: tenemos una clase que implementa totalmente la interfaz [InterfaceDao] que nos hemos impuesto. Ahora podemos pasar a la capa [métier].
11.4. La capa [métier]
Ahora vamos a implementar la capa [2] de nuestra arquitectura:

11.4.1. La interfaz [InterfaceMétier]
La interfaz de la capa [métier] será la siguiente:
<?php
// espacio de nombres
namespace Application;
interface InterfaceMetier {
// cálculo de los impuestos de un contribuyente
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// cálculo de impuestos en modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
Comentarios
- línea 9: la interfaz [InterfaceMétier] es capaz de calcular el importe del impuesto de un contribuyente particular siempre que se le facilite la siguiente información: estado civil, número de hijos, salario anual. El método [calculerImpot] no utiliza la capa [dao], por lo que no lanza excepciones;
- línea 9: la interfaz [InterfaceMétier] también puede calcular el importe del impuesto de un conjunto de contribuyentes cuyos datos se recogen en el archivo de texto denominado [$taxPayersFileName]. Guarda los resultados en un archivo de texto denominado [$resultsFileName]. El método [executeBatchImpots] debe dirigirse a la capa [dao], que se encarga de los accesos al sistema de archivos. En ese caso, pueden producirse excepciones en la capa [dao] que el método [executeBatchImpots] no interceptará: las dejará pasar al script principal. Los errores no fatales se registran en el archivo de texto denominado [$errorsFileName];
- línea 9: el método [calculerImpot] es un método puramente [métier]. No se preocupa por el origen de los datos que utiliza;
- línea 12: el método [executeBatchImpots] se dirigirá a la capa [dao] para leer y escribir datos en archivos de texto. Llamará repetidamente al método de negocio [calculerImpot];
11.4.2. La clase [Metier]
La clase [Metier] implementa la interfaz [InterfaceMetier] de la siguiente manera:
<?php
// espacio de nombres
namespace Application;
class Metier implements InterfaceMetier {
// capa Dao
private $dao;
// datos de la administración tributaria
private $taxAdminData;
//---------------------------------------------
// configurador de capa [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// se almacena una referencia en la capa [dao]
$this->dao = $dao;
// se recuperan los datos que permiten el cálculo del impuesto
// el método [getTaxAdminData] puede lanzar una excepción ExceptionImpots
// se deja que se transmita al código llamante
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// cálculo del impuesto
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// resultado
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
…
// resultado
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=salarioAnual-desgravación
// la deducción tiene un mínimo y un máximo
private function getRevenuImposable(float $salaire): float {
…
// resultado
return floor($revenuImposable);
}
// calcula una posible reducción
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// resultado
return ceil($décôte);
}
// calcula una posible reducción
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
…
// resultado
return ceil($réduction);
}
// cálculo de impuestos en modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
…
// registro de los resultados
$this->dao->saveResults($resultsFileName, $results);
}
}
Comentarios
- línea 6: la clase [Metier] implementa la interfaz [InterfaceMetier], es decir, los métodos [calculerImpot] (líneas 30-34) y [executeBatchImpots] (líneas 66-70);
- línea 8: una referencia a la capa [dao]. Es obligatorio incluir una para que la capa [métier] sepa a quién dirigirse cuando necesite datos externos. Este atributo se inicializará mediante el setter de las líneas 14-17 o mediante el constructor de las líneas 19-26;
- línea 10: el objeto de tipo [TaxAdminData] que encapsula los datos de la administración tributaria. Estos datos son necesarios para el método de negocio [calculerImpot]. Este atributo se inicializa mediante el constructor de las líneas 19-26;
- líneas 19-26: el constructor inicializa los dos atributos de la clase:
- el atributo [$dao] se inicializa con la referencia pasada como parámetro al constructor. Cabe señalar que el tipo de este parámetro es el de la interfaz [InterfaceDao], lo que permite que la clase [Metier] sea inicializada por cualquier clase que implemente esta interfaz;
- el atributo [$taxAdminData] se inicializa llamando al método [getTaxAdminData] de la capa [dao];
Se deduce que, cuando se ejecutan los métodos [calculerImpots] y [executeBatchImpots], se inicializan los dos atributos [$dao] y [$taxAdminData].
El método [calculerImpots] es el siguiente:
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié: sí, no
// $enfants: número de hijos
// $salaire: salario anual
// $this->taxAdminData: datos de la administración tributaria
//
// se comprueba que se dispone de los datos de la administración tributaria
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// cálculo del impuesto con hijos
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// cálculo del impuesto sin hijos
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// aplicación del límite máximo del coeficiente familiar
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros para los dos primeros hijos
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros para los dos primeros hijos, el doble para los siguientes
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// se aplica el impuesto más alto
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// cálculo de una posible deducción
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// cálculo de una posible reducción de impuestos
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// resultado
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
Comentarios
- Este código corresponde al método [AbstractBaseImpots::calculerImpot] de version 3, explicado en el apartado enlace. Lo mismo ocurre con los métodos privados [calculerImpot2, getDecôte, getRéduction, getRevenuImposable];
El método [Metier::executeBatchImpots] es el siguiente:
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// se dejan pasar las excepciones procedentes de la capa [dao]
// se recuperan los datos de los contribuyentes
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabla de resultados
$results = [];
// se procesan
foreach ($taxPayersData as $taxPayerData) {
// se calcula el impuesto
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// se completa [$taxPayerData]
$taxPayerData->setMontant($result["impôt"]);
$taxPayerData->setDécôte($result["décôte"]);
$taxPayerData->setSurCôte($result["surcôte"]);
$taxPayerData->setTaux($result["taux"]);
$taxPayerData->setRéduction($result["réduction"]);
// se introduce el resultado en la tabla de resultados
$results [] = $taxPayerData;
}
// registro de los resultados
$this->dao->saveResults($resultsFileName, $results);
}
Comentarios
- línea 1: el método debe llamar repetidamente al método [calculerImpot] para cada uno de los contribuyentes encontrados en el archivo de texto denominado [$taxPayersFileName]. Debe guardar los resultados en el archivo de texto denominado [$resultsFileName]. Los errores no fatales que se produzcan se registran en el archivo de texto denominado [$errorsFileName]. El método no lanza excepciones por sí mismo, sino que deja que se propaguen las que lanza la capa [dao];
- línea 4: se solicitan los datos de los contribuyentes a la capa [dao]. Esta devuelve una matriz de elementos de tipo [TaxPayerData], que es una clase de atributos [marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux] (véase el párrafo del enlace). Si se produce una excepción aquí, como no es interceptada por un catch, se propagará automáticamente al código llamante. Esto significa que, en caso de excepción, la línea 6 no se ejecuta;
- línea 6: la matriz de resultados de tipo [TaxPayerData];
- líneas 8-22: se calcula el impuesto para cada uno de los elementos de la tabla de contribuyentes [$taxPayersData]. Para ello, se recurre al método interno [calculerImpot] (línea 10);
- líneas 15-19: el resultado obtenido se utiliza para inicializar los atributos de [TaxPayerData] que aún no lo estaban;
- línea 21: el resultado obtenido se acumula en la tabla de resultados [$results];
- línea 24: una vez calculado el impuesto para todos los contribuyentes, los resultados se guardan en un archivo de texto. Esta tarea la realiza la capa [dao];
Conclusión
En general, la capa [métier] es bastante sencilla de escribir, ya que se dirige a la capa [dao], que se encarga de gestionar el acceso a los datos y la gestión de errores correspondiente.
11.5. El script principal
Ahora escribimos el script de la capa [3] de nuestra arquitectura:

El script principal es el siguiente: [main.php]:
<?php
// Se respetan estrictamente los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// gestión de errores mediante PHP
//ini_set("display_errors", "0");
// Inclusión de interfaz y clases
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// prueba -----------------------------------------------------
// definición de constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// creación de la capa [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// creación de la capa [métier]
$métier = new Metier($dao);
// cálculo de impuestos en modo batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// se muestra el error
print $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;
Comentarios
- línea 24: el nombre del archivo de datos de los contribuyentes;
- línea 25: el nombre del archivo de resultados;
- línea 26: el nombre del archivo de errores;
- línea 27: el nombre del archivo jSON que contiene los datos de la administración tributaria;
- línea 31: creación de la capa [dao];
- línea 33: creación de la capa [métier] basada en esta capa [dao];
- línea 35: ejecución del método [executeBatchImpots] de la capa [métier];
- líneas 36-39: hemos visto que la capa [métier] puede generar excepciones. Estas se interceptan aquí;
11.6. Pruebas visuales
11.6.1. Prueba n.º 1
Con el siguiente archivo de contribuyentes [taxpayersdata.txt]:
oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000
se obtiene el siguiente archivo de errores [errors.txt]:
la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
y el siguiente archivo de resultados [resultats.txt]:
11.6.2. Prueba n.º 2
En el script principal, se asigna al archivo de contribuyentes un nombre de archivo que no existe:
Los resultados obtenidos en la consola son los siguientes:
Warning: fopen(taxpayersdata2.txt): failed to open stream: No such file or directory in C:\Data\st-2019\dev\php7\poly\scripts-console\impots\version-04\TraitDao.php on line 18
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
Done.
- línea 1: advertencias (warning) del intérprete PHP;
- línea 2: el mensaje de error de la excepción lanzada por la capa [dao];
Es posible silenciar los mensajes de error del intérprete PHP:

La línea 21 del código anterior solicita que no se muestren los errores PHP. Durante la fase de desarrollo es necesario que se muestren. En modo producción, hay que ocultarlos.
Los resultados de la ejecución son entonces los siguientes:
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
11.7. Pruebas [Codeception]
Las pruebas visuales son muy insuficientes:
- por lo general, nos limitamos a unas pocas pruebas;
- se presta más o menos atención durante esta verificación visual y pueden pasarnos por alto algunos detalles;
En la realidad del desarrollo profesional, las pruebas las redactan personas dedicadas a ello, para quienes es su función principal. Por lo tanto, buscan realizar las pruebas más completas posibles. Para ello, utilizan marcos de pruebas.
Aquí vamos a utilizar el marco Codeception [https://codeception.com/], ya que se puede integrar en Netbeans. Se trata de un marco con un amplio abanico de posibilidades. Nosotros solo vamos a utilizar algunas de ellas. La idea es disponer de un medio rápido, tras cada nueva versión del ejercicio de aplicación, para verificar que esta funciona. La existencia de pruebas superadas da al desarrollador confianza en el código que ha escrito. Es un factor importante.
11.7.1. Instalación del marco [Codeception]
Al igual que muchas bibliotecas PHP, el marco [Codeception] se instala con [Composer]. Por lo tanto, abrimos un terminal Laragon (véase el párrafo del enlace).
En primer lugar, debemos instalar el marco de pruebas PHPUnit [https://phpunit.de/]. De hecho, Codeception utiliza en segundo plano el marco PHPUnit:

A continuación, instalamos el marco Codeception:

Eso es todo. Ahora veamos la integración de [Codeception] en Netbeans.
11.7.2. Integración de [CodeCeption] en Netbeans

- en [1-2], se accede a las propiedades del proyecto;
- en [3-4], se convierte [Codeception] en uno de los marcos de pruebas del proyecto;


- en [5-8], se inicializa el marco [Codeception] para el proyecto;

- En [9], se ha creado una carpeta [tests], así como un archivo de configuración [codeception.yml] en [10-11]. El archivo [11] es el mismo que el archivo [10]. Codeception simplemente ha creado una carpeta [Important Files] para dar un significado especial al archivo [10];
- en [12-13], volvemos a las propiedades del proyecto;

- en [14-16], se designa la carpeta [tests] [16] como la carpeta de pruebas del proyecto;
- en [16], la carpeta [tests] aparece entonces con el nuevo nombre [Test Files]. La presencia de esta carpeta en un proyecto PHP indica que dicho proyecto integra un marco de pruebas programadas;
- crearemos nuestras pruebas en la carpeta [unit] [17];
11.7.3. Pruebas de la capa [dao]

- crearemos todas nuestras pruebas en la carpeta [unit] [1];
- los nombres de las clases de prueba [Codeception] deben terminar con la palabra clave [Test]; de lo contrario, las clases no se reconocerán como clases de prueba;
Nuestras clases de prueba [Codeception] tendrán el siguiente formato [https://codeception.com/docs/05-UnitTests]:
<?php
// Cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// carga del entorno de prueba
…
class DaoTest extends \Codeception\Test\Unit {
// atributos de la prueba
private $attribut1;
public function __construct() {
parent::__construct();
// inicialización del entorno de prueba
…
}
// pruebas
public function testTaxAdminData() {
// pruebas
$this->assertEquals($expected, $actual);
$this->assertEqualsWithDelta($expected, $actual, $delta);
$this->assertTrue($actual);
$this->assertFalse($actual);
$this->assertNull($actual);
$this->assertEmpty($actual);
$this→assertSame($expected, $actual);
…
}
}
Comentarios
- línea 7: las clases de prueba estarán en el mismo espacio de nombres que la aplicación probada;
- líneas 9-10: aquí se encuentran las operaciones [require] para cargar las clases e interfaces probadas;
- línea 12: el nombre de la clase de prueba debe terminar obligatoriamente con la palabra clave [Test]. Esta clase debe extender la clase [\Codeception\Test\Unit];
- líneas 16-20: el constructor nos permitirá inicializar el entorno de prueba;
- línea 23: los nombres de los métodos de prueba deben comenzar obligatoriamente por la palabra clave [test];
- líneas 25-31: se pueden utilizar diversos métodos de prueba;
La clase de prueba [DaoTest] será la siguiente:
<?php
// Cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Inclusión de interfaces y clases
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// prueba -----------------------------------------------------
// definición de constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// creación de la capa [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// pruebas
public function testTaxAdminData() {
…
}
}
Comentarios
Para crear las pruebas de un version del ejercicio de aplicación, utilizaremos un entorno idéntico al utilizado por el script principal del version. El de la version 04 es el siguiente script [main.php]:
<?php
// Cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// gestión de errores mediante PHP
ini_set("display_errors", "0");
// Inclusión de interfaces y clases
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// prueba -----------------------------------------------------
// definición de constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// creación de la capa [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// creación de la capa [métier]
$métier = new Metier($dao);
// cálculo de impuestos en modo batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// se muestra el error
print $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;
Para probar la capa [dao], en la clase de prueba:
- retomamos el entorno de las líneas 13-27 de [main.php];
- en el constructor de la clase de prueba, creamos la capa [dao] como en la línea 31;
- escribimos los métodos de prueba;
Procederemos de esta manera para todas las clases de prueba.
Volvamos al código completo de la clase de prueba:
<?php
// Cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Inclusión de interfaces y clases
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// prueba -----------------------------------------------------
// definición de constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// creación de la capa [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// pruebas
public function testTaxAdminData() {
// constantes de cálculo
$this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
$this->assertEquals(21037, $this->taxAdminData->getPlafondRevenusCelibatairePourReduction());
$this->assertEquals(42074, $this->taxAdminData->getPlafondRevenusCouplePourReduction());
$this->assertEquals(3797, $this->taxAdminData->getValeurReducDemiPart());
$this->assertEquals(1196, $this->taxAdminData->getPlafondDecoteCelibataire());
$this->assertEquals(1970, $this->taxAdminData->getPlafondDecoteCouple());
$this->assertEquals(1595, $this->taxAdminData->getPlafondImpotCelibatairePourDecote());
$this->assertEquals(2627, $this->taxAdminData->getPlafondImpotCouplePourDecote());
$this->assertEquals(12502, $this->taxAdminData->getAbattementDixPourcentMax());
$this->assertEquals(437, $this->taxAdminData->getAbattementDixPourcentMin());
// tramos del impuesto
$this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $this->taxAdminData->getLimites());
$this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $this->taxAdminData->getCoeffR());
$this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $this->taxAdminData->getCoeffN());
}
}
Comentarios
- líneas 10-25: carga del entorno necesario para las pruebas y definiciones de constantes;
- líneas 31-36: construcción de la capa [dao], línea 34, y a continuación inicialización del atributo [$taxAdminData] de la línea 29. Este atributo contiene los datos de la administración tributaria;
- líneas 39-55: el único método de prueba. Este consiste en verificar que el contenido del atributo [$taxAdminData] se corresponde con lo esperado;
- líneas 41-50: comprobaciones de las constantes del cálculo del impuesto;
- líneas 52-55: comprobaciones de los tramos impositivos. El método [assertSame] comprueba que dos entidades PHP, en este caso tablas, sean idénticas;
Para ejecutar esta clase de prueba, se procede de la siguiente manera:

- en [1-2], se ejecuta la prueba;
- [3]: la ventana de resultados de las pruebas;
- [4]: la clase de prueba ejecutada;
- [5]: los resultados. Aquí, el único método de prueba se ha superado;
- [6]: cuando la prueba falla o, más frecuentemente, cuando no se ha ejecutado ninguna prueba, hay que consultar la ventana [6]. En la mayoría de los casos, es la carga del entorno de prueba la que ha fallado y, por lo tanto, no se ha podido ejecutar ninguna prueba. Los errores que se muestran en [6] son los que se producirían al ejecutar un script PHP clásico;
Veamos un ejemplo de prueba errónea:
En la clase de prueba, introducimos un error en la definición de una constante:
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");
y, a continuación, ejecutamos la prueba. El resultado obtenido es el siguiente:

En la ventana [4]:

11.7.4. Pruebas de la capa [métier]
La clase de prueba [MetierTest] sigue las mismas reglas de construcción que la clase [DaoTest], pero hay más métodos de prueba:
<?php
// Cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusión de interfaces y clases
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// prueba -----------------------------------------------------
// definición de constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class MetierTest extends \Codeception\Test\Unit {
// capa de negocio
private $métier;
public function __construct() {
parent::__construct();
// creación de la capa [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
// creación de la capa [métier]
$this->métier = new Metier($dao);
}
// pruebas
public function test1() {
$result = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test2() {
$result = $this->métier->calculerImpot("oui", 2, 50000);
$this->assertEqualsWithDelta(1385, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(384, $result["décôte"], 1);
$this->assertEqualsWithDelta(347, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test3() {
$result = $this->métier->calculerImpot("oui", 3, 50000);
$this->assertEqualsWithDelta(0, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(720, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test4() {
$result = $this->métier->calculerImpot("non", 2, 100000);
$this->assertEqualsWithDelta(19884, $result["impôt"], 1);
$this->assertEqualsWithDelta(4480, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test5() {
$result = $this->métier->calculerImpot("non", 3, 100000);
$this->assertEqualsWithDelta(16782, $result["impôt"], 1);
$this->assertEqualsWithDelta(7176, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test6() {
$result = $this->métier->calculerImpot("oui", 3, 100000);
$this->assertEqualsWithDelta(9200, $result["impôt"], 1);
$this->assertEqualsWithDelta(2180, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.3, $result["taux"]);
}
public function test7() {
$result = $this->métier->calculerImpot("oui", 5, 100000);
$this->assertEqualsWithDelta(4230, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test8() {
$result = $this->métier->calculerImpot("non", 0, 100000);
$this->assertEqualsWithDelta(22986, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test9() {
$result = $this->métier->calculerImpot("oui", 2, 30000);
$this->assertEqualsWithDelta(0, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0, $result["taux"]);
}
public function test10() {
$result = $this->métier->calculerImpot("non", 0, 200000);
$this->assertEqualsWithDelta(64210, $result["impôt"], 1);
$this->assertEqualsWithDelta(7498, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.45, $result["taux"]);
}
public function test11() {
$result = $this->métier->calculerImpot("oui", 3, 200000);
$this->assertEqualsWithDelta(42842, $result["impôt"], 1);
$this->assertEqualsWithDelta(17283, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
}
Comentarios
- líneas 10-25: carga de los archivos que definen el entorno de prueba. Este es el mismo que para la capa [dao];
- líneas 31-37: instanciación de las capas [dao] y [métier];
- líneas 40-47: una prueba de cálculo de impuestos;
- línea 41: se realiza un cálculo de impuestos con la capa [métier];
- líneas 42-46: se comprueba que los resultados obtenidos son los del simulador de la administración tributaria [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm];
- líneas 23-26: las pruebas de igualdad se realizan con una precisión de 1 euro. De hecho, se ha observado que los problemas de redondeo hacían que el algoritmo del documento diera los resultados esperados con una precisión de 1 euro;
- línea 27: el tipo impositivo se calcula sin margen de error;
- líneas 49-137: se repiten este tipo de pruebas 10 veces, cada vez con una configuración diferente del contribuyente;
Las pruebas arrojan los siguientes resultados:

11.7.5. Pruebas de las próximas versiones
A continuación, las pruebas de las capas [dao] y [métier] serán idénticas a las de la version 04. Solo cambiará el entorno de la prueba. Por lo tanto, solo presentaremos este y los resultados de las pruebas.