Skip to content

11. Exercício Prático – Versão 4

A aplicação de cálculo de impostos irá implementar a seguinte arquitetura em camadas:

Image

Iremos reutilizar os elementos da versão 3 da secção associada, modificando-os para os adaptar à nova arquitetura da aplicação. Isto é por vezes designado por «refatoração». Aqui, partimos do princípio de que os dados necessários à aplicação estão armazenados em ficheiros de texto. A camada [Dao] irá gerir as interações com estes ficheiros.

11.1. Árvore de scripts

Image

11.2. Objetos trocados entre camadas

Iremos manter certos objetos da versão 3. Listamo-los aqui como lembrete.

A exceção [ExceptionImpots] é a exceção que a camada [Dao] irá lançar quando encontrar um problema, seja com o acesso aos dados ou com a natureza dos dados (dados incorretos).


<?php
 
// namespace
namespace Application;
 
class ExceptionImpots extends \RuntimeException {
 
  public function __construct(string $message, int $code=0) {
    parent::__construct($message, $code);
  }
}
 

A classe [Utilities] contém métodos úteis para gerir ficheiros de texto (neste caso, um único método):


<?php
 
// namespace
namespace Application;
 
// a class of utility functions
abstract class Utilitaires {
 
  public static function cutNewLinechar(string $ligne): string {
    // delete the end-of-line mark from $ligne if it exists
    $longueur = strlen($ligne);  // line length
    while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
      $ligne = substr($ligne, 0, $longueur - 1);
      $longueur--;
    }
    // end - return the line
    return($ligne);
  }
}
 

A classe [TaxAdminData] é a classe que encapsula os dados de administração fiscal:


<?php
 
namespace Application;
 
class TaxAdminData {
  // tax brackets
  private $limites;
  private $coeffR;
  private $coeffN;
  // tax calculation constants
  private $plafondQfDemiPart;
  private $plafondRevenusCelibatairePourReduction;
  private $plafondRevenusCouplePourReduction;
  private $valeurReducDemiPart;
  private $plafondDecoteCelibataire;
  private $plafondDecoteCouple;
  private $plafondImpotCouplePourDecote;
  private $plafondImpotCelibatairePourDecote;
  private $abattementDixPourcentMax;
  private $abattementDixPourcentMin;
 
  // initialization
  public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
    // retrieve the contents of the tax data file
    $fileContents = \file_get_contents($taxAdminDataFilename);

    // we return the object
    return $this;
  }
 
  private function check($value): \stdClass {

    return $result;
  }
 
    // toString
  public function __toString() {
    // object's Json string
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }
 
  // getters and 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 ficheiro de resultados:


<?php
 
// namespace
namespace Application;
 
// data class
class TaxPayerData {
  // data required to calculate the taxpayer's tax liability
  private $marié;
  private $enfants;
  private $salaire;
  // tax calculation results
  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{
    // taxpayer data required for tax calculation
    $this->marié = $marié;
    $this->enfants = $nbEnfants;
    $this->salaire = $salaireAnnuel;
    // initialize the object
    return $this;
  }
 
  // getters and setters
  public function getMarié() {
    return $this->marié;
  }
 

 
  public function setMarié($marié) {
    $this->marié = $marié;
    return $this;
  }
 

 
    // toString
  public function __toString() {
    // object's Json string
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }
 
}
 

Nota: Utilize a geração automática de código para gerar o construtor, os getters e os setters (consulte a secção indicada no link). Note que os setters são «fluentes».

11.3. A camada [DAO]

Aqui, estamos a concentrar-nos na camada [1] da nossa aplicação:

Image

11.3.1. A interface [InterfaceDao]

A interface para a camada [DAO] será a seguinte [InterfaceDao.php]:


<?php
 
// namespace
namespace Application;
 
interface InterfaceDao {
 
  // reading taxpayer data
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
 
  // reading tax data (tax brackets)
  public function getTaxAdminData(): TaxAdminData;
 
  // recording results
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
 

Comentários

  • Os requisitos são os seguintes:
    • Os dados do contribuinte são armazenados num ficheiro de texto;
    • os resultados do cálculo do imposto são guardados num ficheiro de texto;
    • Quaisquer erros são guardados num ficheiro de texto;
    • não se sabe em que formato os dados da autoridade fiscal estão disponíveis. Para cada novo formato, a interface [InterfaceDao] deve ser implementada por uma nova classe;
    • os métodos da interface que encontram um erro fatal ao aceder aos dados devem lançar uma exceção do tipo [TaxException];
  • linha 9: o método que recupera os dados do contribuinte [estado civil, número de filhos, salário anual];
    • o primeiro parâmetro é o nome do ficheiro de texto que contém estes dados;
    • o segundo parâmetro é o nome do ficheiro de texto no qual se devem registar quaisquer erros encontrados;
  • linha 12: o método que recupera dados da autoridade fiscal. Aqui não são passados parâmetros porque não sabemos como os dados estão armazenados;
  • linha 15: o método utilizado para guardar os resultados do cálculo do imposto num ficheiro de texto, cujo nome é passado como parâmetro;

Ao escrever a interface [InterfaceDao], sabemos que haverá diferentes formas de implementar o método [getTaxAdminData], dependendo de como os dados da administração fiscal estão armazenados. A interface [InterfaceDao] será, portanto, implementada por diferentes classes, cada uma a lidar com um método de armazenamento específico para estes dados (matrizes, ficheiros de texto, bases de dados, serviços web). Estas classes derivadas partilharão, no entanto, código comum, especificamente a implementação dos métodos [getTaxPayersData] e [saveResults]. Sabemos que este caso de utilização pode ser implementado de duas formas (ver parágrafo em destaque):

  1. Criamos uma classe abstrata C que contém 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. Criamos então as classes C1 e C2 derivadas de C, cada uma das quais implementa os métodos não definidos (abstratos) da sua classe pai C à sua maneira;
  2. criamos uma característica T que é quase idêntica à classe abstrata C da solução anterior. Esta característica não implementa a interface I porque, sintaticamente, não o pode fazer. Criamos então as classes C1 e C2 que implementam a interface I e utilizam a característica T. Tudo o que resta a estas classes é implementar os métodos da interface I que não são implementados pela característica T que importam;

Para este exemplo, utilizaremos aqui um trait [TraitDao].

11.3.2. O trait [TraitDao]

O código para o trait [TraitDao] é o seguinte [TraitDao.php]:


<?php
 
// namespace
namespace Application;
 
trait TraitDao {
 
  // reading taxpayer data
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
    // taxpayer data table
    $taxPayersData = [];
    // error table
    $errors = [];
    // many errors can occur when managing files
    try {
      // reading user data
      // each line has the form marital status, number of children, annual salary
      $taxPayersFile = fopen($taxPayersFilename, "r");
      if (!$taxPayersFile) {
        throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
      }
      // the current line of the user data file is used
      // in the form of marital status, number of children, annual salary
      $num = 1;         // n° current line
      $nbErreurs = 0;   // number of errors encountered
      while ($ligne = fgets($taxPayersFile, 100)) {
        // empty lines are neglected
        $ligne = trim($ligne);
        if (strlen($ligne) == 0) {
          // next line
          $num++;
          // we loop again
          continue;
        }
        // remove any end-of-line marker
        $ligne = Utilitaires::cutNewLineChar($ligne);
        // we retrieve the 3 fields married:children:salary which form $ligne
        list($marié, $enfants, $salaire) = explode(",", $ligne);
        // we check them
        // marital status must be yes or no
        $marié = trim(strtolower($marié));
        $erreur = ($marié !== "oui" and $marié !== "non");
        if (!$erreur) {
          // the number of children must be an integer
          $enfants = trim($enfants);
          if (!preg_match("/^\d+$/", $enfants)) {
            $erreur = TRUE;
          } else {
            $enfants = (int) $enfants;
          }
        }
        if (!$erreur) {
          // the salary is a whole number without the euro cents
          $salaire = trim($salaire);
          if (!preg_match("/^\d+$/", $salaire)) {
            $erreur = TRUE;
          } else {
            $salaire = (int) $salaire;
          }
        }
        // mistake?
        if ($erreur) {
          $errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
          $nbErreurs++;
        } else {
          // memorize information
          $taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
        }
        // next line
        $num++;
      }
      // are we at the end of the file?
      if (!feof($taxPayersFile)) {
        // we're out of the loop on a read error
        throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
      } else {
        // we're out of the loop at the end-of-file mark
        // save errors in a text file
        $this->saveString($errorsFilename, implode("\n", $errors));
        // function result
        return $taxPayersData;
      }
    } finally {
      // close the file if it is open
      if ($taxPayersFile) {
        fclose($taxPayersFile);
      }
    }
  }
 
  // recording results
  public function saveResults(string $resultsFilename, array $taxPayersData): void {
    // save table [$taxPayersData] in text file [$resultsFileName]
    // if text file [$resultsFileName] does not exist, it is created
    $this->saveString($resultsFilename, implode("\n", $taxPayersData));
  }
 
  // saving table results in a text file
  private function saveString(string $fileName, string $data): void {
    // save table [$data] in text file [$fileName]
    // if text file [$fileName] does not exist, it is created
    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: aqui definimos uma característica, não uma classe;
  • linhas 9–89: O método [getTaxPayersData] implementa o método com o mesmo nome da interface [InterfaceDao]. Ele recupera dados dos contribuintes [estado civil, número de filhos, rendimento anual ] de um ficheiro de texto denominado [$taxPayersFilename]. Ele devolve estes dados como uma matriz [$taxPayersData] de elementos do tipo [TaxPayerData] (linhas 67, 81);
  • o método [getTaxPayersData] é muito semelhante ao método [AbstractBaseImpots::executeBatchImpots] descrito na secção referenciada, com as seguintes diferenças:
    • o método [getTaxPayersData] apenas recupera dados dos contribuintes. Não realiza quaisquer cálculos fiscais. Aqui, essa é a função da camada [business];
    • Tal como o método [executeBatchImpots], reporta erros. Aqui, os erros são primeiro armazenados numa matriz [$errors] (linha 13), que é depois guardada num ficheiro de texto no final do processo (linha 79). Dependendo da situação, esta matriz pode ou não estar vazia;
    • no caso de um erro fatal, é lançada uma exceção [ExceptionImpots] (linhas 20, 75);
  • linha 73: repare no processamento realizado ao sair do ciclo nas linhas 26–71. De facto, a função [fgets] tem a desvantagem de devolver o valor booleano FALSE tanto quando a leitura das linhas encontra o marcador de fim de ficheiro como quando a leitura falha devido a um erro. Para distinguir entre os dois casos, verificamos se chegámos ao fim do ficheiro utilizando a função [feof]. Se não tivermos chegado ao fim do ficheiro, isso significa que ocorreu um erro e, nesse caso, lançamos uma exceção;
  • linhas 83–88: o bloco [finally] é executado independentemente de ter ocorrido uma exceção durante o processamento do ficheiro;
  • linha 85: se o ficheiro tiver sido aberto, então o «handle» do ficheiro [$taxPayersFile] tem o valor booleano TRUE; caso contrário, é FALSE;
  • linhas 99–105: o método privado [saveString] utilizado na linha 79 para guardar a matriz de erros num ficheiro de texto;
  • linha 99: o método [saveString] recebe dois parâmetros:
    • [string $filename], que é o nome do ficheiro de texto utilizado para guardar os dados;
    • [string $data], que é a string a ser guardada no ficheiro de texto. Esta string será uma sequência de linhas terminadas pelo caractere de nova linha \n;
  • linha 102: a função PHP [file_puts_contents] escreve uma string num ficheiro de texto. Abre o ficheiro, escreve a string nele e fecha o ficheiro. Retorna FALSE se ocorrer 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]. O método privado [saveString] é utilizado novamente. Aqui, o segundo parâmetro de [saveString] é uma cadeia de caracteres construída a partir da matriz [$taxPayersData], cujos elementos são do tipo [TaxPayerData]. Poder-se-á questionar qual será o resultado da operação:

implode("\n", $taxPayersData)

Definimos o seguinte método [__toString] na classe [TaxPayerData] (ver secção em link):


public function __toString() {
    // chaîne Json de l'objet
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

A operação


implode("\n", $taxPayersData)

irá concatenar cada elemento da matriz [$taxPayersData] — convertido numa cadeia de caracteres pelo seu método [__toString] — com o caractere de nova linha \n. Isto resultará numa cadeia de caracteres com o seguinte formato:

json1\njson2\n…

Conclusão

A característica [TraitDao] implementou dois dos métodos da interface [InterfaceDao], [getTaxPayersData] e [saveResults]:


<?php
 
// namespace
namespace Application;
 
interface InterfaceDao {
 
  // reading taxpayer data
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
 
  // reading tax data (tax brackets)
  public function getTaxAdminData(): TaxAdminData;
 
  // recording results
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
 

Ainda precisamos de implementar o método [getTaxAdminData], que recupera dados da administração fiscal.

11.3.3. A classe [ImpotsWithTaxAdminDataInJsonFile]

A classe [ImpotsWithTaxAdminDataInJsonFile] implementa a interface [InterfaceDao] da seguinte forma:


<?php
 
// namespace
namespace Application;
 
// definition of a ImpotsWithDataInFile class
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
  // use of a line
  use TraitDao;
  // the TaxAdminData object containing tax bracket data
  private $taxAdminData;
 
  // the manufacturer
  public function __construct(string $taxAdminDataFilename) {
    // we want to initialize the [$this->taxAdminData] attribute
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
  }

  // returns data for tax calculation
  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] da interface [InterfaceDao]. Resta, portanto, que a classe [ImpotsWithTaxAdminDataInJsonFile] implemente o método [getTaxAdminData], que recupera dados da administração fiscal;
  • Linha 11: o atributo do tipo [TaxAdminData] devolvido pelo método [getTaxAdminData] nas linhas 20–22. Este atributo é inicializado pelo construtor nas linhas 14–17;

Concluímos agora a camada [DAO] da nossa aplicação: temos uma classe que implementa totalmente a interface [InterfaceDao] que definimos. Podemos agora passar para a camada [business].

11.4. A camada [business]

Vamos agora implementar a camada [2] da nossa arquitetura:

Image

11.4.1. A interface [InterfaceMétier]

A interface para a camada [business] será a seguinte:


<?php
 
// namespace
namespace Application;
 
interface InterfaceMetier {
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
 

Comentários

  • linha 9: a interface [BusinessInterface] pode calcular o valor do imposto para um contribuinte individual, desde que lhe sejam fornecidas as seguintes informações: estado civil, número de filhos, salário anual. O método [calculateTax] não utiliza a camada [DAO], pelo que não lança exceções;
  • Linha 9: A interface [BusinessInterface] também pode calcular o valor do imposto para um grupo de contribuintes cujos dados estão reunidos no ficheiro de texto denominado [$taxPayersFileName]. Ela grava os resultados num ficheiro de texto denominado [$resultsFileName]. O método [executeBatchImpots] deve comunicar com a camada [dao], que gere o acesso ao sistema de ficheiros. As exceções podem então ser propagadas a partir da camada [dao], que o método [executeBatchImpots] não irá capturar: permitirá que se propaguem para o script principal. Os erros não fatais são registados no ficheiro de texto denominado [$errorsFileName];
  • linha 9: o método [calculateTax] é um método puramente [business]. Não se preocupa com a origem dos dados que utiliza;
  • linha 12: o método [executeBatchImpots] irá interagir com a camada [dao] para ler e escrever dados em ficheiros de texto. Irá chamar repetidamente o método de negócio [calculerImpot];

11.4.2. A classe [Business]

A classe [Metier] implementa a interface [InterfaceMetier] da seguinte forma:


<?php
 
// namespace
namespace Application;
 
class Metier implements InterfaceMetier {
  // dao layer
  private $dao;
  // tax administration data
  private $taxAdminData;
 
  //---------------------------------------------
  // setter couche [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }
 
  public function __construct(InterfaceDao $dao) {
    // a reference is stored on the [dao] layer
    $this->dao = $dao;
    // recover data for tax calculation
    // method [getTaxAdminData] may throw a ExceptionImpots exception
    // we then let it go back to the calling code
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }
 
// tAX CALCULATION
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // result
    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 {

    // result
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }
 
  // revenuImposable=annualwage-discount
  // the allowance has a minimum and a maximum
  private function getRevenuImposable(float $salaire): float {

    // result
    return floor($revenuImposable);
  }
 
// calculates any discount
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // result
    return ceil($décôte);
  }
 
// calculates any reduction
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {

    // result
    return ceil($réduction);
  }
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {

    // recording results
    $this->dao->saveResults($resultsFileName, $results);
  }
 
}
 

Comentários

  • linha 6: a classe [Business] implementa a interface [BusinessInterface], ou seja, os métodos [calculateTax] (linhas 30–34) e [executeBatchTaxes] (linhas 66–70);
  • linha 8: uma referência à camada [dao]. Isto é necessário para que a camada [business] saiba onde procurar quando precisar de dados externos. Este atributo será inicializado através do setter nas linhas 14–17 ou através do construtor nas linhas 19–26;
  • linha 10: o objeto do tipo [TaxAdminData] que encapsula os dados de administração fiscal. Estes dados são necessários para o método de negócio [calculateTax]. Este atributo é inicializado através do construtor nas linhas 19–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. Note-se que o tipo deste parâmetro é o da interface [InterfaceDao], permitindo que a classe [Metier] seja inicializada por qualquer classe que implemente esta interface;
    • o atributo [$taxAdminData] é inicializado chamando o método [getTaxAdminData] da camada [dao];

Concluímos que, quando os métodos [calculateTaxes] e [executeBatchTaxes] são executados, ambos os atributos [$dao] e [$taxAdminData] são inicializados.

O método [calculateTaxes] é o seguinte:


public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié : oui, non
    // $enfants : nombre d'enfants
    // $salaire : salaire annuel
    // $this->taxAdminData : données de l'administration fiscale
    //
    // on vérifie qu'on a bien les données de l'administration fiscale
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // calcul de l'impôt avec enfants
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // calcul de l'impôt sans les enfants
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // application du plafonnement du quotient familial
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // on prend l'impôt le plus fort
    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"];
    }
    // calcul d'une éventuelle décôte
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // calcul d'une éventuelle réduction d'impôts
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // résultat
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }
 

Comentários

  • Este código provém do método [AbstractBaseImpots::calculateTax] da versão 3, explicado na secção indicada no link. O mesmo se aplica aos métodos privados [calculateTax2, getDiscount, getReduction, getTaxableIncome];

O método [Metier::executeBatchImpots] é o seguinte:


public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // we let the exceptions coming from the [dao] layer flow upwards
    // retrieve taxpayer data
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // results table
    $results = [];
    // we exploit them
    foreach ($taxPayersData as $taxPayerData) {
      // tax calculation
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // complete [$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"]);
      // put the result in the results table
      $results [] = $taxPayerData;
    }
    // recording results
    $this->dao->saveResults($resultsFileName, $results);
  }
 

Comentários

  • linha 1: o método deve chamar repetidamente o método [calculateTax] para cada contribuinte encontrado no ficheiro de texto denominado [$taxPayersFileName]. Deve gravar os resultados no ficheiro de texto denominado [$resultsFileName]. Os erros não fatais encontrados são registados no ficheiro de texto denominado [$errorsFileName]. O método não lança exceções por si próprio, mas permite que as exceções lançadas pela camada [dao] se propaguem;
  • Linha 4: os dados dos contribuintes são solicitados à camada [dao]. Isto devolve uma matriz de elementos do tipo [TaxPayerData], que é uma classe de atributos [married, numberOfChildren, salary, amount, deduction, reduction, surcharge, rate] (ver parágrafo em link). Se ocorrer uma exceção aqui, uma vez que não é capturada por um bloco catch, propagar-se-á automaticamente de volta para o código de chamada. Isto significa que, no caso de uma exceção, a linha 6 não é executada;
  • linha 6: a matriz de resultados do tipo [TaxPayerData];
  • linhas 8–22: o imposto é calculado para cada elemento da matriz de contribuintes [$taxPayersData]. Para tal, é chamado o método interno [calculateTax] (linha 10);
  • linhas 15–19: o resultado é utilizado para inicializar os atributos de [TaxPayerData] que ainda não tinham sido inicializados;
  • linha 21: o resultado é adicionado à matriz de resultados [$results];
  • linha 24: assim que o imposto for calculado para todos os contribuintes, os resultados são guardados num ficheiro de texto. A camada [dao] encarrega-se desta tarefa;

Conclusão

Em geral, a camada [business] é bastante simples de escrever, pois faz interface com a camada [DAO], que gere o acesso aos dados juntamente com o tratamento de erros associado.

11.5. O script principal

Vamos agora escrever o script para a camada [3] da nossa arquitetura:

Image

O script principal é o seguinte [main.php]:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;

// error handling by PHP
//ini_set("display_errors", "0");
 
// interface and class inclusion
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";
// test -----------------------------------------------------
// definition of constants
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
 
try {
  // creation of the [dao] layer
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // creation of the [business] layer
  $métier = new Metier($dao);
  // tax calculation in batch mode
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // error is displayed
  print $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;
 
 

Comentários

  • Linha 24: o nome do ficheiro de dados do contribuinte;
  • linha 25: o nome do ficheiro de resultados;
  • linha 26: o nome do ficheiro de erros;
  • linha 27: o nome do ficheiro JSON que contém os dados da autoridade fiscal;
  • linha 31: criação da camada [dao];
  • linha 33: criação da camada [business] com base nesta camada [dao];
  • linha 35: execução do método [executeBatchImpots] da camada [business];
  • linhas 36–39: vimos que a camada [business] pode lançar exceções. Estas são capturadas aqui;

11.6. Testes visuais

11.6.1. Teste n.º 1

Com o seguinte ficheiro de contribuintes [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

Obtemos o seguinte ficheiro 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 seguinte ficheiro de resultados [resultats.txt]:

1
2
3
4
5
6
7
8
9
{"marié":"oui","enfants":2,"salaire":55555,"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"oui","enfants":2,"salaire":50000,"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":3,"salaire":50000,"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
{"marié":"non","enfants":2,"salaire":100000,"impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":3,"salaire":100000,"impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"marié":"non","enfants":0,"salaire":100000,"impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":2,"salaire":30000,"impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}
{"marié":"non","enfants":0,"salaire":200000,"impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}
{"marié":"oui","enfants":3,"salaire":200000,"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}

11.6.2. Teste n.º 2

No script principal, atribuímos um nome de ficheiro que não existe ao ficheiro do contribuinte:

const TAXPAYERS_DATA_FILENAME = "taxpayersdata2.txt";

Os resultados apresentados na consola sã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 do interpretador PHP;
  • linha 2: mensagem de erro da exceção lançada pela camada [dao];

É possível suprimir as mensagens de erro do interpretador PHP:

Image

A linha 21 do código acima instrui o sistema a não exibir erros de PHP. Durante a fase de desenvolvimento, é necessário que estes sejam exibidos. No modo de produção, devem ser ocultados.

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 inadequados:

  • geralmente limitamo-nos a apenas alguns testes;
  • podemos não prestar atenção suficiente durante esta inspeção visual, e alguns detalhes podem escapar-nos;

No mundo real do desenvolvimento profissional, os testes são elaborados por indivíduos dedicados para quem esta é a função principal. Eles esforçam-se por tornar os testes o mais abrangentes possível. Para tal, utilizam estruturas de testes.

Aqui, utilizaremos a estrutura Codeception [https://codeception.com/] porque pode ser integrada no NetBeans. É uma estrutura com uma vasta gama de capacidades. Utilizaremos apenas algumas delas. A ideia é ter uma forma rápida, após cada nova versão do exercício da aplicação, de verificar se esta funciona. A existência de testes bem-sucedidos dá ao programador confiança no código que escreveu. Este é um fator importante.

11.7.1. Instalação da estrutura [Codeception]

Tal como muitas bibliotecas PHP, o framework [Codeception] é instalado utilizando o [Composer]. Por isso, abrimos um terminal Laragon (ver link no parágrafo).

Primeiro, precisamos de instalar a estrutura de testes PHPUnit [https://phpunit.de/]. Isto porque o Codeception utiliza a estrutura PHPUnit nos bastidores:

Image

Em seguida, instalamos o framework Codeception:

Image

É isso. Agora vamos ver como integrar o [Codeception] no NetBeans.

11.7.2. Integrar o [Codeception] no NetBeans

Image

  • Em [1-2], aceda às propriedades do projeto;
  • Em [3-4], definimos o [Codeception] como uma das estruturas de testes do projeto;

Image

Image

  • Em [5-8], inicialize a estrutura [Codeception] para o projeto;

Image

  • Em [9], foi criada uma pasta [tests], juntamente com um ficheiro de configuração [codeception.yml] em [10-11]. O ficheiro [11] é idêntico ao ficheiro [10]. O Codeception limitou-se a criar uma pasta [Important Files] para atribuir ao ficheiro [10] uma designação especial;
  • em [12-13], voltamos às propriedades do projeto;

Image

  • em [14-16], a pasta [tests] [16] é designada como a pasta de testes do projeto;
  • em [16], a pasta [tests] aparece então com o novo nome [Test Files]. A presença desta pasta num projeto PHP indica que o projeto incorpora uma estrutura de testes unitários;
  • vamos criar os nossos testes na pasta [unit] [17];

11.7.3. Testes para a camada [dao]

Image

  • Criaremos 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;

As nossas classes de teste [Codeception] terão o seguinte formato [https://codeception.com/docs/05-UnitTests]:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// loading the test environment

 
class DaoTest extends \Codeception\Test\Unit {
  // test attributes
  private $attribut1;
 
  public function __construct() {
    parent::__construct();
    // test environment initialization

  }
 
  // tests
  public function testTaxAdminData() {
    // tests
    $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 namespace que a aplicação em teste;
  • linhas 9–10: aqui estão as instruções [require] para carregar as classes e interfaces testadas;
  • linha 12: o nome da classe de teste deve terminar com a palavra-chave [Test]. Esta classe deve estender a classe [\Codeception\Test\Unit];
  • linhas 16–20: o construtor permite-nos inicializar o ambiente de teste;
  • linha 23: os nomes dos métodos de teste devem começar com a palavra-chave [test];
  • linhas 25–31: podem ser utilizados vários métodos de teste;

A classe de teste [DaoTest] será a seguinte:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// interface and class inclusion
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";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";

class DaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;
 
  public function __construct() {
    parent::__construct();
    // creation of the [dao] layer
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  public function testTaxAdminData() {

  }
}
 

Comentários

Para criar os testes para uma versão do exercício da aplicação, utilizaremos um ambiente idêntico ao utilizado pelo script principal da versão. Para a versão 04, trata-se do seguinte script [main.php]:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// error handling by PHP
ini_set("display_errors", "0");
 
// interface and class inclusion
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";
// test -----------------------------------------------------
// definition of constants
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
 
try {
  // creation of the [dao] layer
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // creation of the [business] layer
  $métier = new Metier($dao);
  // tax calculation in batch mode
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // error is displayed
  print $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;

Para testar a camada [dao], na classe de teste:

  • utilizamos o ambiente das linhas 13–27 de [main.php];
  • no construtor da classe de teste, instanciamos a camada [dao] como na linha 31;
  • escrevemos os métodos de teste;

Procederemos desta forma para todas as classes de teste.

Voltemos ao código completo da classe de teste:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// interface and class inclusion
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";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";
 
class DaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;
 
  public function __construct() {
    parent::__construct();
    // creation of the [dao] layer
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  public function testTaxAdminData() {
    // calculation constants
    $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());
    // tax brackets
    $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 testes e definição de constantes;
  • linhas 31–36: construção da camada [dao] (linha 34), seguida da inicialização do atributo [$taxAdminData] (linha 29). Este atributo contém dados de administração fiscal;
  • linhas 39–55: o único método de teste. Consiste em verificar se o conteúdo do atributo [$taxAdminData] corresponde ao esperado;
  • linhas 41–50: verificações das constantes de cálculo de impostos;
  • linhas 52–55: verificações das faixas de imposto. O método [assertSame] verifica se duas entidades PHP — neste caso, matrizes — são idênticas;

Para executar esta classe de teste, proceda da seguinte forma:

Image

  • em [1-2], execute o teste;
  • [3]: a janela de resultados do teste;
  • [4]: a classe de teste executada;
  • [5]: os resultados. Aqui, o único método de teste foi aprovado;
  • [6]: quando o teste falha, ou mais frequentemente quando nenhum teste foi executado, verifique a janela [6]. Na maioria das vezes, o ambiente de teste não conseguiu carregar, pelo que nenhum teste pôde ser executado. Os erros apresentados em [6] são os mesmos que veria ao executar um script PHP padrão;

Vejamos um exemplo de um teste com falha:

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");

depois executamos o teste. O resultado é o seguinte:

Image

Na janela [4]:

Image

11.7.4. Testes da Camada [Business]

A classe de teste [MetierTest] segue as mesmas regras de construção que a classe [DaoTest], mas possui mais métodos de teste:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// interface and class inclusion
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";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";
 
class MetierTest extends \Codeception\Test\Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // creation of the [dao] layer
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    // creation of the [business] layer
    $this->métier = new Metier($dao);
  }
 
  // tests
  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 ficheiros que definem o ambiente de teste. Isto é igual ao da camada [dao];
  • linhas 31–37: instanciação das camadas [dao] e [business];
  • linhas 40–47: um teste de cálculo de impostos;
  • linha 41: é realizado um cálculo de imposto específico utilizando a camada [business];
  • linhas 42–46: verificação de que os resultados obtidos correspondem aos do simulador da autoridade fiscal [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm];
  • linhas 23–26: são realizados testes de igualdade com uma margem de erro de 1 euro. De facto, observámos que problemas de arredondamento fizeram com que o algoritmo do documento produzisse os resultados esperados com uma margem de erro de 1 euro;
  • linha 27: a taxa de imposto é calculada sem qualquer margem de erro;
  • linhas 49–137: este tipo de teste é repetido 10 vezes, cada vez com uma configuração diferente do contribuinte;

Os testes produzem os seguintes resultados:

Image

11.7.5. Testes para versões futuras

Daqui em diante, os testes para as camadas [dao] e [business] serão idênticos aos da versão 04. Apenas o ambiente de teste irá mudar. Apresentaremos, portanto, apenas este ambiente e os resultados dos testes.