11. Exercício prático – versão 4
O aplicativo de cálculo de impostos implementará a seguinte estrutura em camadas:

Vamos retomar os elementos da versão 3 do parágrafo “link”, modificando-os para adaptá-los à nova arquitetura do aplicativo. Às vezes, isso é chamado de “refatoração”. Supomos aqui que os dados necessários ao aplicativo estejam em arquivos de texto. É a camada [Dao] que se encarregará das trocas com esses arquivos.
11.1. Estrutura dos scripts

11.2. Objetos trocados entre camadas
Manteremos alguns objetos da versão 3. Repetimos aqui para lembrança.
A exceção [ExceptionImpots] é a exceção que será lançada pela camada [Dao] quando ela encontrar um problema, seja com o acesso aos dados, seja com a natureza dos dados (dados incorretos).
<?php
// espaço de nomes
namespace Application;
class ExceptionImpots extends \RuntimeException {
public function __construct(string $message, int $code=0) {
parent::__construct($message, $code);
}
}
A classe [Utilitaires] reúne métodos úteis para o gerenciamento de arquivos de texto (neste caso, um único método):
<?php
// espaço de nomes
namespace Application;
// uma classe de funções utilitárias
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
// remove-se o marcador de fim de linha de $ligne, caso exista
$longueur = strlen($ligne); // comprimento da linha
while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
$ligne = substr($ligne, 0, $longueur - 1);
$longueur--;
}
// fim — a linha é restaurada
return($ligne);
}
}
A classe [TaxAdminData] é a classe que encapsula os dados da administração fiscal:
<?php
namespace Application;
class TaxAdminData {
// faixas de imposto
private $limites;
private $coeffR;
private $coeffN;
// constantes de cálculo do imposto
private $plafondQfDemiPart;
private $plafondRevenusCelibatairePourReduction;
private $plafondRevenusCouplePourReduction;
private $valeurReducDemiPart;
private $plafondDecoteCelibataire;
private $plafondDecoteCouple;
private $plafondImpotCouplePourDecote;
private $plafondImpotCelibatairePourDecote;
private $abattementDixPourcentMax;
private $abattementDixPourcentMin;
// inicialização
public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
// recuperação do conteúdo do arquivo de dados fiscais
$fileContents = \file_get_contents($taxAdminDataFilename);
…
// retorna o objeto
return $this;
}
private function check($value): \stdClass {
…
return $result;
}
// toString
public function __toString() {
// cadeia JSON do objeto
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
// getters e setters
public function getLimites() {
return $this->limites;
}
…
public function setLimites($limites) {
$this->limites = $limites;
return $this;
}
…
}
Adicionamos uma nova classe [TaxPayerData] que encapsula os dados gravados no arquivo de resultados:
<?php
// espaço de nomes
namespace Application;
// a classe de dados
class TaxPayerData {
// dados necessários para o cálculo do imposto do contribuinte
private $marié;
private $enfants;
private $salaire;
// resultados do cálculo do imposto
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{
// dados do contribuinte necessários para o cálculo do imposto
$this->marié = $marié;
$this->enfants = $nbEnfants;
$this->salaire = $salaireAnnuel;
// retorna o objeto inicializado
return $this;
}
// getters e setters
public function getMarié() {
return $this->marié;
}
…
public function setMarié($marié) {
$this->marié = $marié;
return $this;
}
…
// toString
public function __toString() {
// cadeia JSON do objeto
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
}
Observação: utilize a geração automática de código para gerar o construtor, os getters e os setters (consulte o parágrafo com o link). Observe que os setters são “fluentes”.
11.3. A camada [dao]
Aqui, estamos interessados na camada [1] do nosso aplicativo:

11.3.1. A interface [InterfaceDao]
A interface da camada [dao] será a seguinte: [InterfaceDao.php]:
<?php
// espaço de nomes
namespace Application;
interface InterfaceDao {
// leitura dos dados dos contribuintes
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// leitura dos dados da administração tributária (faixas de imposto)
public function getTaxAdminData(): TaxAdminData;
// gravação dos resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Comentários
- As especificações são as seguintes:
- os dados dos contribuintes estão em um arquivo de texto;
- os resultados do cálculo dos impostos são gravados em um arquivo de texto;
- os eventuais erros são registrados em um arquivo de texto;
- não se sabe em que formato os dados da administração fiscal estão disponíveis. Para cada novo formato, a interface [InterfaceDao] deverá ser implementada por uma nova classe;
- os métodos da interface que encontrarem um erro irrecuperável ao acessar os dados devem lançar uma exceção do tipo [ExceptionImpots];
- linha 9: o método que permite obter os dados do contribuinte [statut marital, nombre d’enfants, salaire annuel];
- o primeiro parâmetro é o nome do arquivo de texto no qual esses dados estão contidos;
- o segundo parâmetro é o nome do arquivo de texto no qual devem ser registrados os eventuais erros encontrados;
- linha 12: o método que permite obter os dados da administração fiscal. Aqui, não se passa nenhum parâmetro a ele, pois não se sabe como esses dados estão armazenados;
- linha 15: o método que permite registrar os resultados do cálculo do imposto em um arquivo de texto cujo nome é passado como parâmetro;
Ao escrever a interface [InterfaceDao], sabemos que haverá diferentes maneiras de escrever o método [getTaxAdminData], dependendo de como os dados da administração fiscal serão armazenados. A interface [InterfaceDao] será, portanto, implementada por diferentes classes, cada uma responsável por um tipo específico de armazenamento desses dados (tabelas, arquivos de texto, banco de dados, serviço web). Essas classes derivadas terão, no entanto, um código comum: o da implementação dos métodos [getTaxPayersData, saveResults]. Sabe-se que esse caso de uso pode ser implementado de duas maneiras (ver parágrafo com link):
- cria-se uma classe abstrata C que reúne o código comum às classes derivadas. A classe C implementa a interface I, mas certos métodos que devem ser declarados nas classes derivadas são declarados como abstratos na classe C e, portanto, a própria classe C é abstrata. Em seguida, criam-se as classes C1 e C2, derivadas de C, que implementam, cada uma à sua maneira, os métodos não definidos (abstratos) de sua classe pai C;
- cria-se um trait T quase idêntico à classe abstrata C da solução anterior. Esse trait não implementa a interface I, pois, sintaticamente, não é possível fazê-lo. Em seguida, criam-se as classes C1 e C2, que implementam a interface I e utilizam o traço T. Essas classes precisam apenas implementar os métodos da interface I que não foram implementados pelo traço T que elas importam;
Para o exemplo, vamos utilizar aqui um trait [TraitDao].
11.3.2. O trait [TraitDao]
O código do trait [TraitDao] é o seguinte: [TraitDao.php]:
<?php
// espaço de nomes
namespace Application;
trait TraitDao {
// leitura dos dados dos contribuintes
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// tabela de dados dos contribuintes
$taxPayersData = [];
// tabela de erros
$errors = [];
// podem ocorrer vários erros ao gerenciar arquivos
try {
// leitura dos dados do usuário
// cada linha tem o formato: estado civil, número de filhos, salário anual
$taxPayersFile = fopen($taxPayersFilename, "r");
if (!$taxPayersFile) {
throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
}
// processa-se a linha atual do arquivo de dados do usuário
// que tem o formato: estado civil, número de filhos, salário anual
$num = 1; // nº da linha atual
$nbErreurs = 0; // número de erros encontrados
while ($ligne = fgets($taxPayersFile, 100)) {
// ignora-se as linhas vazias
$ligne = trim($ligne);
if (strlen($ligne) == 0) {
// linha seguinte
$num++;
// retorna ao início do ciclo
continue;
}
// remove-se a eventual marca de fim de linha
$ligne = Utilitaires::cutNewLineChar($ligne);
// recuperam-se os três campos “casado:filhos:salário” que formam $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// verifica-se
// o estado civil deve ser “sim” ou “não”
$marié = trim(strtolower($marié));
$erreur = ($marié !== "oui" and $marié !== "non");
if (!$erreur) {
// o número de filhos deve ser um número inteiro
$enfants = trim($enfants);
if (!preg_match("/^\d+$/", $enfants)) {
$erreur = TRUE;
} else {
$enfants = (int) $enfants;
}
}
if (!$erreur) {
// o salário é um número inteiro, sem centavos de euro
$salaire = trim($salaire);
if (!preg_match("/^\d+$/", $salaire)) {
$erreur = TRUE;
} else {
$salaire = (int) $salaire;
}
}
// erro?
if ($erreur) {
$errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
$nbErreurs++;
} else {
// as informações são armazenadas
$taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
}
// próxima linha
$num++;
}
// chegamos ao fim do arquivo?
if (!feof($taxPayersFile)) {
// saímos do loop devido a um erro de leitura
throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
} else {
// saímos do loop ao encontrar o marcador de fim de arquivo
// os erros são salvos em um arquivo de texto
$this->saveString($errorsFilename, implode("\n", $errors));
// resultado da função
return $taxPayersData;
}
} finally {
// fecha-se o arquivo, caso esteja aberto
if ($taxPayersFile) {
fclose($taxPayersFile);
}
}
}
// gravação dos resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// gravação da tabela [$taxPayersData] no arquivo de texto [$resultsFileName]
// se o arquivo de texto [$resultsFileName] não existir, ele será criado
$this->saveString($resultsFilename, implode("\n", $taxPayersData));
}
// gravação dos resultados de uma tabela em um arquivo de texto
private function saveString(string $fileName, string $data): void {
// gravação da tabela [$data] no arquivo de texto [$fileName]
// se o arquivo de texto [$fileName] não existir, ele será criado
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
}
}
}
Comentários
- linha 6: definimos aqui um traço e não uma classe;
- linhas 9-89: o método [getTaxPayersData] implementa o método de mesmo nome da interface [InterfaceDao]. Ele recupera, em um arquivo de texto chamado [$taxPayersFilename], os dados dos contribuintes [statut marital, nombre d’enfants, salaire annuel]. Ela apresenta esses dados na forma de uma tabela [$taxPayersData] com elementos do tipo [TaxPayerData] (linhas 67, 81);
- o método [getTaxPayersData] é muito semelhante ao método [AbstractBaseImpots::executeBatchImpots] descrito no parágrafo “link”, com as seguintes diferenças:
- o método [getTaxPayersData] limita-se a recuperar os dados dos contribuintes. Ele não realiza cálculos de imposto. Nesse caso, essa função cabe à camada [métier];
- assim como fazia o método [executeBatchImpots], ele sinaliza os erros. Aqui, os erros são primeiro armazenados em uma tabela [$errors] (linha 13), tabela essa que é gravada em um arquivo de texto ao final do processamento (linha 79). Dependendo do caso, ela pode estar vazia ou não;
- no caso de erro irrecuperável, é lançada uma exceção do tipo [ExceptionImpots] (linhas 20, 75);
- linha 73: observe-se o processamento realizado ao sair do ciclo das linhas 26-71. De fato, a função [fgets] tem a desvantagem de retornar o valor booleano FALSE tanto quando a leitura das linhas encontra o marcador de fim de arquivo quanto quando essa leitura não pôde ser concluída devido a um erro. Para diferenciar os dois casos, verifica-se se chegou ao fim do arquivo com a função [feof]. Se não se chegou ao fim do arquivo, significa que ocorreu um erro e, nesse caso, lança-se uma exceção;
- linhas 83-88: a função [finally] é executada independentemente de ter ocorrido ou não uma exceção durante o processamento do arquivo;
- linha 85: se o arquivo foi aberto, o ‘handle’ [$taxPayersFile] do arquivo assume o valor booleano TRUE; caso contrário, assume o valor FALSE;
- linhas 99-105: o método privado [saveString], utilizado na linha 79 para gravar a tabela de erros em um arquivo de texto;
- linha 99: o método [saveString] recebe dois parâmetros:
- [string $filename], que é o nome do arquivo de texto usado para gravar os dados;
- [string $data], que é a sequência de caracteres a ser gravada no arquivo de texto. Essa sequência será um conjunto de linhas terminadas pelo caractere de fim de linha \n;
- linha 102: a função PHP [file_puts_contents] grava uma sequência de caracteres em um arquivo de texto. Ela se encarrega de abrir o arquivo, gravar a sequência nele e fechar o arquivo. Ela retorna o valor booleano FALSE caso ocorra um erro;
- linha 103: se ocorrer um erro, é lançada uma exceção;
- linhas 92-96: implementação do método [saveResults] da interface [InterfaceDao]. Utiliza-se novamente o método privado [saveString]. Aqui, o segundo parâmetro de [saveString] é uma string construída a partir da matriz [$taxPayersData], cujos elementos são do tipo [TaxPayerData]. Podemos nos perguntar qual será o resultado da operação:
implode("\n", $taxPayersData)
Definimos na classe [TaxPayerData] (parágrafo “link”) o seguinte método [__toString]:
public function __toString() {
// cadeia JSON do objeto
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
A operação
implode("\n", $taxPayersData)
irá concatenar cada elemento da matriz [$taxPayersData], transformado em sequência de caracteres pelo método [__toString], com o caractere de fim de linha \n. Isso resultará em uma sequência de caracteres no formato:
json1\njson2\n…
Conclusão
A função [TraitDao] implementou dois dos métodos da interface [InterfaceDao], [getTaxPayersData] e [saveResults]:
<?php
// espaço de nomes
namespace Application;
interface InterfaceDao {
// leitura dos dados dos contribuintes
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// leitura dos dados da administração tributária (faixas de imposto)
public function getTaxAdminData(): TaxAdminData;
// gravação dos resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Resta-nos implementar o método [getTaxAdminData], que recupera os dados da administração fiscal.
11.3.3. A classe [ImpotsWithTaxAdminDataInJsonFile]
A classe [ImpotsWithTaxAdminDataInJsonFile] implementa a interface [InterfaceDao] da seguinte maneira:
<?php
// espaço de nomes
namespace Application;
// definição de uma classe ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
// uso de uma característica
use TraitDao;
// o objeto do tipo TaxAdminData que contém os dados das faixas de imposto
private $taxAdminData;
// o construtor
public function __construct(string $taxAdminDataFilename) {
// deseja-se inicializar o atributo [$this->taxAdminData]
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// retorna os dados que permitem o cálculo do imposto
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Comentários
- linha 7: a classe [ImpotsWithTaxAdminDataInJsonFile] implementa a interface [InterfaceDao];
- linha 9: a classe [ImpotsWithTaxAdminDataInJsonFile] utiliza o traço [traitDao], que, como sabemos, implementa os métodos [getTaxPayersData] e [saveResults] dainterface [InterfaceDao]. Portanto, resta à classe [ImpotsWithTaxAdminDataInJsonFile] apenas implementar o método [getTaxAdminData], que recupera os dados da administração fiscal;
- linha 11: o atributo do tipo [TaxAdminData], que é retornado pelo método [getTaxAdminData] das linhas 20 a 22. Esse atributo é inicializado pelo construtor das linhas 14 a 17;
Concluímos a camada [dao] de nossa aplicação: temos uma classe que implementa totalmente a interface [InterfaceDao] que definimos. Agora podemos passar para a camada [métier].
11.4. A camada [métier]
Agora vamos implementar a camada [2] da nossa arquitetura:

11.4.1. A interface [InterfaceMétier]
A interface da camada [métier] será a seguinte:
<?php
// espaço de nomes
namespace Application;
interface InterfaceMetier {
// cálculo dos impostos de um contribuinte
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// cálculo de impostos em modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
Comentários
- linha 9: a interface [InterfaceMétier] é capaz de calcular o valor do imposto de um contribuinte pessoa física, desde que sejam fornecidas as seguintes informações: estado civil, número de filhos, salário anual. O método [calculerImpot] não utiliza a camada [dao], portanto não gera exceções;
- linha 9: a interface [InterfaceMétier] também pode calcular o valor do imposto de um conjunto de contribuintes cujos dados estão reunidos no arquivo de texto denominado [$taxPayersFileName]. Ela grava os resultados em um arquivo de texto denominado [$resultsFileName]. O método [executeBatchImpots] deve se comunicar com a camada [dao], responsável pelo acesso ao sistema de arquivos. Exceções podem, então, ser propagadas a partir da camada [dao], mas o método [executeBatchImpots] não as interceptará: ele permitirá que elas sejam propagadas para o script principal. Os erros não fatais são registrados no arquivo de texto chamado [$errorsFileName];
- linha 9: o método [calculerImpot] é um método puramente [métier]. Ele não se preocupa com a origem dos dados que utiliza;
- linha 12: o método [executeBatchImpots] recorrerá à camada [dao] para ler e gravar dados em arquivos de texto. Ele chamará repetidamente o método de negócio [calculerImpot];
11.4.2. A classe [Metier]
A classe [Metier] implementa a interface [InterfaceMetier] da seguinte maneira:
<?php
// espaço de nomes
namespace Application;
class Metier implements InterfaceMetier {
// camada DAO
private $dao;
// dados da administração tributária
private $taxAdminData;
//---------------------------------------------
// setter da camada [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// armazena-se uma referência na camada [dao]
$this->dao = $dao;
// recuperam-se os dados necessários para o cálculo do imposto
// o método [getTaxAdminData] pode lançar uma exceção ExceptionImpots
// em seguida, permite que a exceção seja repassada ao código chamador
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// cálculo do imposto
// --------------------------------------------------------------------------
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=salárioAnual-abatimento
// a dedução tem um valor mínimo e um valor máximo
private function getRevenuImposable(float $salaire): float {
…
// resultado
return floor($revenuImposable);
}
// calcula uma eventual redução
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// resultado
return ceil($décôte);
}
// calcula uma eventual redução
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
…
// resultado
return ceil($réduction);
}
// cálculo de impostos em modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
…
// gravação dos resultados
$this->dao->saveResults($resultsFileName, $results);
}
}
Comentários
- linha 6: a classe [Metier] implementa a interface [InterfaceMetier], ou seja, os métodos [calculerImpot] (linhas 30-34) e [executeBatchImpots] (linhas 66-70);
- linha 8: uma referência à camada [dao]. É obrigatório que haja uma referência para que a camada [métier] saiba a quem se dirigir quando precisar de dados externos. Esse atributo será inicializado por meio do setter das linhas 14 a 17 ou por meio do construtor das linhas 19 a 26;
- linha 10: o objeto do tipo [TaxAdminData] que encapsula os dados da administração fiscal. Esses dados são necessários para o método de negócio [calculerImpot]. Esse atributo é inicializado por meio do construtor nas linhas 19 a 26;
- linhas 19-26: o construtor inicializa os dois atributos da classe:
- o atributo [$dao] é inicializado com a referência passada como parâmetro ao construtor. Observe-se que o tipo desse parâmetro é o da interface [InterfaceDao], permitindo assim que a classe [Metier] seja inicializada por qualquer classe que implemente essa interface;
- o atributo [$taxAdminData] é inicializado por meio do método [getTaxAdminData] da camada [dao];
Conclui-se que, quando os métodos [calculerImpots] e [executeBatchImpots] são executados, os dois atributos [$dao] e [$taxAdminData] são inicializados.
O método [calculerImpots] é o seguinte:
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié: sim, não
// $enfants: número de filhos
// $salaire: salário anual
// $this->taxAdminData: dados da administração fiscal
//
// verifica-se se os dados da administração fiscal estão corretos
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// cálculo do imposto com filhos
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// cálculo do imposto sem filhos
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// aplicação do limite máximo do quociente familiar
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos, o dobro para os seguintes
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// considera-se o imposto mais 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 uma eventual dedução
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// cálculo de uma eventual redução de impostos
$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];
}
Comentários
- esse código é o do método [AbstractBaseImpots::calculerImpot] da versão 3, explicado no parágrafo “link”. O mesmo se aplica aos métodos privados [calculerImpot2, getDecôte, getRéduction, getRevenuImposable];
O método [Metier::executeBatchImpots] é o seguinte:
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// permitimos que as exceções provenientes da camada [dao] sejam propagadas
// recuperam-se os dados dos contribuintes
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabela de resultados
$results = [];
// analisamos os resultados
foreach ($taxPayersData as $taxPayerData) {
// calcula-se o imposto
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// preenche-se [$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"]);
// insere-se o resultado na tabela de resultados
$results [] = $taxPayerData;
}
// gravação dos resultados
$this->dao->saveResults($resultsFileName, $results);
}
Comentários
- linha 1: o método deve chamar repetidamente o método [calculerImpot] para cada um dos contribuintes encontrados no arquivo de texto denominado [$taxPayersFileName]. Ele deve gravar os resultados no arquivo de texto denominado [$resultsFileName]. Os erros não fatais encontrados são registrados no arquivo de texto denominado [$errorsFileName]. O método não lança exceções por conta própria, mas permite que sejam propagadas as exceções lançadas pela camada [dao];
- linha 4: os dados dos contribuintes são solicitados à camada [dao]. Esta retorna um array de elementos do tipo [TaxPayerData], que é uma classe de atributos [marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux] (ver parágrafo “link”). Se ocorrer uma exceção aqui, como ela não é interceptada por um catch, ela será automaticamente repassada para o código chamador. Isso significa que, em caso de exceção, a linha 6 não é executada;
- linha 6: a matriz de resultados do tipo [TaxPayerData];
- linhas 8-22: calcula-se o imposto para cada um dos elementos da matriz de contribuintes [$taxPayersData]. Para isso, recorre-se ao método interno [calculerImpot] (linha 10);
- linhas 15-19: o resultado obtido é utilizado para inicializar os atributos de [TaxPayerData] que ainda não haviam sido inicializados;
- linha 21: o resultado obtido é acumulado na tabela de resultados [$results];
- linha 24: uma vez calculado o imposto para todos os contribuintes, os resultados são gravados em um arquivo de texto. Essa tarefa é realizada pela camada [dao];
Conclusão
Em geral, a camada [métier] é bastante simples de escrever, pois se comunica com a camada [dao], que, por sua vez, gerencia o acesso aos dados e o tratamento de erros associado a ele.
11.5. O script principal
Vamos agora escrever o script da camada [3] da nossa arquitetura:

O script principal é o seguinte: [main.php]:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// gestão de erros por PHP
//ini_set("display_errors", "0");
// inclusão de interface e classes
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";
// teste -----------------------------------------------------
// definição das constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// criação da camada [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// criação da camada [métier]
$métier = new Metier($dao);
// cálculo de impostos em modo batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// exibição do erro
print $ex->getMessage() . "\n";
}
// fim
print "Terminé\n";
exit;
Comentários
- linha 24: o nome do arquivo com os dados dos contribuintes;
- linha 25: o nome do arquivo de resultados;
- linha 26: o nome do arquivo de erros;
- linha 27: o nome do arquivo jSON contendo os dados da administração fiscal;
- linha 31: criação da camada [dao];
- linha 33: criação da camada [métier] com base nessa camada [dao];
- linha 35: execução do método [executeBatchImpots] da camada [métier];
- linhas 36-39: vimos que a camada [métier] pode gerar exceções. Elas são interceptadas aqui;
11.6. Testes visuais
11.6.1. Teste nº 1
Com o arquivo de contribuintes [taxpayersdata.txt] a seguir:
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
obtém-se o seguinte arquivo de erros [errors.txt]:
la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
e o arquivo de resultados [resultats.txt] a seguir:
11.6.2. Teste nº 2
No script principal, atribui-se ao arquivo dos contribuintes um nome que não existe:
Os resultados exibidos no console são, então, os seguintes:
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.
- linha 1: avisos (warning) do interpretador PHP;
- linha 2: mensagem de erro da exceção lançada pela camada [dao];
É possível silenciar as mensagens de erro do interpretador PHP:

A linha 21 do código acima determina que os erros PHP não sejam exibidos. Durante a fase de desenvolvimento, é necessário que eles sejam exibidos. No modo de produção, é preciso ocultá-los.
Os resultados da execução são, então, os seguintes:
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
11.7. Testes [Codeception]
Os testes visuais são muito insuficientes:
- geralmente limitam-se a alguns testes;
- a atenção prestada durante essa verificação visual varia, e alguns detalhes podem passar despercebidos;
Na prática do desenvolvimento profissional, os testes são elaborados por profissionais dedicados, cuja função principal é essa. Eles buscam, portanto, realizar os testes mais completos possíveis. Para isso, utilizam frameworks de teste.
Vamos utilizar aqui o framework Codeception [https://codeception.com/], pois ele pode ser integrado ao NetBeans. Trata-se de um framework com uma ampla gama de possibilidades. Vamos utilizar apenas algumas delas. A ideia é ter uma maneira rápida, após cada nova versão do exercício de aplicação, de verificar se ela funciona. A existência de testes bem-sucedidos dá ao desenvolvedor confiança no código que escreveu. Esse é um fator importante.
11.7.1. Instalação do framework [Codeception]
Assim como muitas bibliotecas PHP, o framework [Codeception] é instalado junto com o [Composer]. Portanto, abrimos um terminal do Laragon (ver parágrafo com o link).
Primeiramente, precisamos instalar o framework de testes PHPUnit [https://phpunit.de/]. Na verdade, o Codeception utiliza, nos bastidores, o framework PHPUnit:

Em seguida, instalamos o framework Codeception:

É isso. Agora, vamos ver a integração do [Codeception] no NetBeans.
11.7.2. Integração do [CodeCeption] no NetBeans

- no [1-2], acessamos as propriedades do projeto;
- em [3-4], definimos [Codeception] como uma das estruturas de teste do projeto;


- No [5-8], inicializa-se o framework [Codeception] para o projeto;

- no [9], foi criada uma pasta [tests], bem como um arquivo de configuração [codeception.yml] no [10-11]. O arquivo [11] é o mesmo que o arquivo [10]. O Codeception simplesmente criou uma pasta [Important Files] para atribuir um significado específico ao arquivo [10];
- no [12-13], voltamos às propriedades do projeto;

- em [14-16], a pasta [tests] [16] é designada como a pasta de testes do projeto;
- em [16], a pasta [tests] passa a aparecer com o novo nome [Test Files]. A presença dessa pasta em um projeto PHP indica que esse projeto integra uma estrutura de testes programados;
- criaremos nossos testes na pasta [unit] [17];
11.7.3. Testes da camada [dao]

- vamos criar todos os nossos testes na pasta [unit] [1];
- os nomes das classes de teste [Codeception] devem terminar com a palavra-chave [Test]; caso contrário, as classes não serão reconhecidas como classes de teste;
Nossas classes de teste [Codeception] terão o seguinte formato: [https://codeception.com/docs/05-UnitTests]:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// carregamento do ambiente de teste
…
class DaoTest extends \Codeception\Test\Unit {
// atributos do teste
private $attribut1;
public function __construct() {
parent::__construct();
// inicialização do ambiente de teste
…
}
// testes
public function testTaxAdminData() {
// testes
$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);
…
}
}
Comentários
- linha 7: as classes de teste estarão no mesmo espaço de nomes que a aplicação testada;
- linhas 9-10: aqui estão as operações [require] para carregar as classes e interfaces testadas;
- linha 12: o nome da classe de teste deve, obrigatoriamente, terminar com a palavra-chave [Test]. Essa classe deve estender a classe [\Codeception\Test\Unit];
- linhas 16-20: o construtor nos permitirá inicializar o ambiente de teste;
- linha 23: os nomes dos métodos de teste devem obrigatoriamente começar com a palavra-chave [test];
- linhas 25-31: diversos métodos de teste podem ser utilizados;
A classe de teste [DaoTest] será a seguinte:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
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");
// inclusão de interface e classes
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";;
// teste -----------------------------------------------------
// definição de constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// criação da camada [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// testes
public function testTaxAdminData() {
…
}
}
Comentários
Para criar os testes de uma versão do exercício de aplicação, utilizaremos um ambiente idêntico ao utilizado pelo script principal da versão. O da versão 04 é o seguinte script [main.php]:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// gestão de erros por PHP
ini_set("display_errors", "0");
// inclusão de interface e classes
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";
// teste -----------------------------------------------------
// definição de constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// criação da camada [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// criação da camada [métier]
$métier = new Metier($dao);
// cálculo de impostos em modo batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// exibição do erro
print $ex->getMessage() . "\n";
}
// fim
print "Terminé\n";
exit;
Para testar a camada [dao], na classe de teste:
- retomamos o ambiente das linhas 13 a 27 do [main.php];
- no construtor da classe de teste, criamos a camada [dao] conforme a linha 31;
- escrevemos os métodos de teste;
Procederemos dessa forma para todas as classes de teste.
Voltemos ao código completo da classe de teste:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
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");
// inclusão de interface e classes
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";;
// teste -----------------------------------------------------
// definição de constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// criação da camada [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// testes
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());
// faixas de imposto
$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());
}
}
Comentários
- linhas 10-25: carregamento do ambiente necessário para os testes e definições de constantes;
- linhas 31-36: construção da camada [dao], linha 34, seguida da inicialização do atributo [$taxAdminData] da linha 29. Esse atributo contém os dados da administração fiscal;
- linhas 39-55: o único método de teste. Este consiste em verificar se o conteúdo do atributo [$taxAdminData] corresponde ao esperado;
- linhas 41-50: verificações das constantes do cálculo do imposto;
- linhas 52-55: verificações das faixas de tributação. O método [assertSame] verifica se duas entidades PHP, neste caso tabelas, são idênticas;
Para executar essa classe de teste, proceda da seguinte maneira:

- em [1-2], executa-se o teste;
- [3]: a janela de resultados dos testes;
- [4]: a classe de teste executada;
- [5]: os resultados. Aqui, o único método de teste foi bem-sucedido;
- [6]: quando o teste falha ou, mais frequentemente, quando nenhum teste foi executado, é preciso consultar a janela [6]. Na maioria das vezes, foi o carregamento do ambiente de teste que falhou e, portanto, nenhum teste pôde ser executado. Os erros exibidos em [6] são os mesmos que ocorreriam na execução de um script PHP convencional;
Vejamos um exemplo de teste com erro:
Na classe de teste, introduzimos um erro na definição de uma constante:
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");
e, em seguida, executamos o teste. O resultado obtido é o seguinte:

Na janela [4]:

11.7.4. Testes da camada [métier]
A classe de teste [MetierTest] segue as mesmas regras de construção que a classe [DaoTest], mas possui mais métodos de teste:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
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");
// inclusão de interface e classes
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";;
// teste -----------------------------------------------------
// definição de constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class MetierTest extends \Codeception\Test\Unit {
// camada de negócios
private $métier;
public function __construct() {
parent::__construct();
// criação da camada [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
// criação da camada [métier]
$this->métier = new Metier($dao);
}
// testes
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"]);
}
}
Comentários
- linhas 10-25: carregamento dos arquivos que definem o ambiente de teste. Esse ambiente é o mesmo da camada [dao];
- linhas 31-37: instanciação das camadas [dao] e [métier];
- linhas 40-47: um teste de cálculo de imposto;
- linha 41: é realizado um determinado cálculo de imposto com a camada [métier];
- linhas 42-46: verifica-se se os resultados obtidos são os do simulador da administração tributária [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm];
- linhas 23-26: os testes de igualdade são realizados com uma tolerância de 1 euro. De fato, observou-se que problemas de arredondamento faziam com que o algoritmo do documento apresentasse os resultados esperados com uma tolerância de 1 euro;
- linha 27: a alíquota de imposto é calculada sem margem de erro;
- linhas 49-137: esse tipo de teste é repetido 10 vezes, cada vez com uma configuração diferente do contribuinte;
Os testes apresentam os seguintes resultados:

11.7.5. Testes das próximas versões
A seguir, os testes das camadas [dao] e [métier] serão idênticos aos da versão 04. A única mudança será o ambiente de teste. Portanto, apresentaremos apenas este e os resultados dos testes.