Skip to content

11. Практичне завдання — версія 4

Додаток для розрахунку податків реалізує таку багаторівневу структуру:

Image

Ми візьмемо елементи з версії 3, описані в розділі «Посилання», та модифікуємо їх, щоб адаптувати до нової архітектури додатка. Іноді це називають «рефакторингом». Тут ми припускаємо, що дані, необхідні для додатка, містяться у текстових файлах. Саме шар [Dao] відповідатиме за обмін даними з цими файлами.

11.1. Структура скриптів

Image

11.2. Об’єкти, що обмінюються між рівнями

Ми збережемо деякі об’єкти з версії 3. Наводимо їх тут для нагадування.

Виняток [ExceptionImpots] — це виняток, який запускатиме шар [Dao], коли він зіткнеться з проблемою або з доступом до даних, або з характером даних (неправильні дані).


<?php

// простір імен
namespace Application;

class ExceptionImpots extends \RuntimeException {

  public function __construct(string $message, int $code=0) {
    parent::__construct($message, $code);
  }
}

Клас [Utilitaires] об’єднує методи, корисні для роботи з текстовими файлами (у даному випадку — один метод):


<?php

// простір імен
namespace Application;

// клас допоміжних функцій
abstract class Utilitaires {

  public static function cutNewLinechar(string $ligne): string {
    // видаляється маркер кінця рядка з $ligne, якщо він існує
    $longueur = strlen($ligne);  // довжина рядка
    while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
      $ligne = substr($ligne, 0, $longueur - 1);
      $longueur--;
    }
    // кінець — повертається рядок
    return($ligne);
  }
}

Клас [TaxAdminData] — це клас, що інкапсулює дані податкової адміністрації:


<?php

namespace Application;

class TaxAdminData {
  // податкові шкали
  private $limites;
  private $coeffR;
  private $coeffN;
  // константи для розрахунку податку
  private $plafondQfDemiPart;
  private $plafondRevenusCelibatairePourReduction;
  private $plafondRevenusCouplePourReduction;
  private $valeurReducDemiPart;
  private $plafondDecoteCelibataire;
  private $plafondDecoteCouple;
  private $plafondImpotCouplePourDecote;
  private $plafondImpotCelibatairePourDecote;
  private $abattementDixPourcentMax;
  private $abattementDixPourcentMin;

  // ініціалізація
  public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
    // отримання вмісту файлу податкових даних
    $fileContents = \file_get_contents($taxAdminDataFilename);

    // повертається об’єкт
    return $this;
  }

  private function check($value): \stdClass {

    return $result;
  }

    // toString
  public function __toString() {
    // JSON-рядок об’єкта
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

  // методи getter та setter
  public function getLimites() {
    return $this->limites;
  }



  public function setLimites($limites) {
    $this->limites = $limites;
    return $this;
  }


}

Ми додаємо новий клас [TaxPayerData], який інкапсулює дані, записані у файл результатів:


<?php

// простір імен
namespace Application;

// клас даних
class TaxPayerData {
  // дані, необхідні для розрахунку податку платника податків
  private $marié;
  private $enfants;
  private $salaire;
  // результати розрахунку податку
  private $montant;
  private $surcôte;
  private $décôte;
  private $réduction;
  private $taux;

  // сетер
  public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel) : TaxPayerData{
    // дані платника податків, необхідні для розрахунку податку
    $this->marié = $marié;
    $this->enfants = $nbEnfants;
    $this->salaire = $salaireAnnuel;
    // повертається ініціалізований об’єкт
    return $this;
  }

  // гетери та сеттери
  public function getMarié() {
    return $this->marié;
  }



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



    // toString
  public function __toString() {
    // JSON-рядок об’єкта
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

}

Примітка: скористайтеся автоматичним генеруванням коду для створення конструктора, методів getter та setter (див. параграф за посиланням). Зверніть увагу, що методи setter є «флюентними».

11.3. Рівень [dao]

Тут нас цікавить рівень [1] нашого додатка:

Image

11.3.1. Інтерфейс [InterfaceDao]

Інтерфейс шару [dao] буде таким: [InterfaceDao.php]:


<?php

// простір імен
namespace Application;

interface InterfaceDao {

  // читання даних про платників податків
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // читання даних податкової адміністрації (податкові ставки)
  public function getTaxAdminData(): TaxAdminData;

  // запис результатів
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

Коментарі

  • Технічне завдання виглядає так:
    • дані платників податків містяться у текстовому файлі;
    • результати розрахунку податків записуються у текстовий файл;
    • можливі помилки записуються у текстовий файл;
    • невідомо, у якому форматі доступні дані податкової адміністрації. Для кожного нового формату інтерфейс [InterfaceDao] повинен бути реалізований новим класом;
    • методи інтерфейсу, які під час доступу до даних стикаються з непереборною помилкою, повинні генерувати виняток типу [ExceptionImpots];
  • рядок 9: метод, що дозволяє отримати дані платника податків [statut marital, nombre d’enfants, salaire annuel];
    • перший параметр — це ім’я текстового файлу, у якому містяться ці дані;
    • другий параметр — це ім’я текстового файлу, у якому слід записати можливі помилки;
  • рядок 12: метод, що дозволяє отримати дані від податкової служби. Тут йому не передається жодного параметра, оскільки невідомо, як саме вони зберігаються;
  • рядок 15: метод, що дозволяє зберегти результати розрахунку податку у текстовому файлі, ім’я якого передається як параметр;

Під час написання інтерфейсу [InterfaceDao] відомо, що існуватимуть різні варіанти реалізації методу [getTaxAdminData] залежно від способу зберігання даних податкової служби. Отже, інтерфейс [InterfaceDao] буде реалізовано різними класами, кожен з яких відповідатиме за конкретний спосіб зберігання цих даних (таблиці, текстові файли, база даних, веб-сервіс). Проте ці похідні класи матимуть спільний код — код реалізації методів [getTaxPayersData, saveResults]. Відомо, що цей варіант використання можна реалізувати двома способами (див. параграф за посиланням):

  1. створюється абстрактний клас C, який об’єднує код, спільний для похідних класів. Клас C реалізує інтерфейс I, але деякі методи, які мають бути оголошені в похідних класах, у класі C оголошені як абстрактні, а отже, сам клас C є абстрактним. Потім створюються класи C1 та C2, похідні від C, які кожен по-своєму реалізують невизначені (абстрактні) методи свого батьківського класу C;
  2. створюється риса T, майже ідентична абстрактному класу C з попереднього рішення. Ця риса не реалізує інтерфейс I, оскільки синтаксично вона не може цього зробити. Далі створюються класи C1 та C2, які реалізують інтерфейс I та використовують рис T. Цим класам залишається лише реалізувати ті методи інтерфейсу I, які не реалізовані рисою T, яку вони імпортують;

Для прикладу ми використаємо тут рис [TraitDao].

11.3.2. Трайт [TraitDao]

Код риси [TraitDao] має такий вигляд: [TraitDao.php]:


<?php

// простір імен
namespace Application;

trait TraitDao {

  // читання даних платників податків
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
    // таблиця даних платників податків
    $taxPayersData = [];
    // таблиця помилок
    $errors = [];
    // під час роботи з файлами може виникнути чимало помилок
    try {
      // зчитування даних користувача
      // кожен рядок має такий формат: сімейний стан, кількість дітей, річний оклад
      $taxPayersFile = fopen($taxPayersFilename, "r");
      if (!$taxPayersFile) {
        throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
      }
      // обробляється поточний рядок файлу даних користувача
      // яка має такий формат: сімейний стан, кількість дітей, річний оклад
      $num = 1;         // номер поточного рядка
      $nbErreurs = 0;   // кількість виявлених помилок
      while ($ligne = fgets($taxPayersFile, 100)) {
        // порожні рядки ігноруються
        $ligne = trim($ligne);
        if (strlen($ligne) == 0) {
          // наступний рядок
          $num++;
          // повертаємося до початку циклу
          continue;
        }
        // видаляється можливий символ кінця рядка
        $ligne = Utilitaires::cutNewLineChar($ligne);
        // отримуємо 3 поля «одружений:діти:зарплата», які утворюють $ligne
        list($marié, $enfants, $salaire) = explode(",", $ligne);
        // перевіряємо їх
        // сімейний стан має бути «так» або «ні»
        $marié = trim(strtolower($marié));
        $erreur = ($marié !== "oui" and $marié !== "non");
        if (!$erreur) {
          // кількість дітей має бути цілим числом
          $enfants = trim($enfants);
          if (!preg_match("/^\d+$/", $enfants)) {
            $erreur = TRUE;
          } else {
            $enfants = (int) $enfants;
          }
        }
        if (!$erreur) {
          // зарплата — ціле число без центів
          $salaire = trim($salaire);
          if (!preg_match("/^\d+$/", $salaire)) {
            $erreur = TRUE;
          } else {
            $salaire = (int) $salaire;
          }
        }
        // помилка?
        if ($erreur) {
          $errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
          $nbErreurs++;
        } else {
          // інформація зберігається
          $taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
        }
        // наступний рядок
        $num++;
      }
      // чи досягнуто кінця файлу?
      if (!feof($taxPayersFile)) {
        // цикл завершено через помилку читання
        throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
      } else {
        // цикл завершено через позначку кінця файлу
        // зберігаємо помилки у текстовому файлі
        $this->saveString($errorsFilename, implode("\n", $errors));
        // результат функції
        return $taxPayersData;
      }
    } finally {
      // файл закривається, якщо він відкритий
      if ($taxPayersFile) {
        fclose($taxPayersFile);
      }
    }
  }

  // запис результатів
  public function saveResults(string $resultsFilename, array $taxPayersData): void {
    // запис масиву [$taxPayersData] у текстовий файл [$resultsFileName]
    // якщо текстовий файл [$resultsFileName] не існує, його створюють
    $this->saveString($resultsFilename, implode("\n", $taxPayersData));
  }

  // запис результатів таблиці у текстовий файл
  private function saveString(string $fileName, string $data): void {
    // запис таблиці [$data] у текстовий файл [$fileName]
    // якщо текстовий файл [$fileName] не існує, його створюють
    if (file_put_contents($fileName, $data) === FALSE) {
      throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
    }
  }

}

Коментарі

  • рядок 6: тут ми визначаємо рис, а не клас;
  • рядки 9–89: метод [getTaxPayersData] реалізує однойменний метод інтерфейсу [InterfaceDao]. Він отримує з текстового файлу з назвою [$taxPayersFilename] дані про платників податків [statut marital, nombre d’enfants, salaire annuel]. Вона повертає ці дані у вигляді масиву [$taxPayersData] з елементами типу [TaxPayerData] (рядки 67, 81);
  • метод [getTaxPayersData] дуже схожий на метод [AbstractBaseImpots::executeBatchImpots], описаний у розділі «Посилання», з такими відмінностями:
    • метод [getTaxPayersData] лише отримує дані про платників податків. Він не здійснює розрахунку податку. Цю роль тут виконує рівень [métier];
    • як і метод [executeBatchImpots], він повідомляє про помилки. Тут помилки спочатку зберігаються в масиві [$errors] (рядок 13), який після завершення обробки записується у текстовий файл (рядок 79). Залежно від обставин, він може бути порожнім або заповненим;
    • у разі непереборної помилки генерується виняток типу [ExceptionImpots] (рядки 20, 75);
  • рядок 73: зверніть увагу на обробку, що виконується після виходу з циклу рядків 26–71. Справа в тому, що функція [fgets] має недолік: вона повертає логічне значення FALSE як у випадку, коли під час зчитування рядків було досягнуто позначки кінця файлу, так і в разі, якщо зчитування не вдалося завершити через помилку. Щоб розрізнити ці два випадки, за допомогою функції [feof] перевіряється, чи досягнуто кінця файлу. Якщо кінець файлу не досягнуто, це означає, що сталася помилка, і тоді генерується виняток;
  • рядки 83–88: функція [finally] виконується незалежно від того, чи сталося виключення під час обробки файлу;
  • рядок 85: якщо файл було відкрито, то «дескриптор» [$taxPayersFile] цього файлу має булеве значення TRUE, в іншому випадку — FALSE;
  • рядки 99–105: приватний метод [saveString], який використовується в рядку 79 для запису масиву помилок у текстовий файл;
  • рядок 99: метод [saveString] приймає два параметри:
    • [string $filename] — це ім’я текстового файлу, що використовується для запису даних;
    • [string $data] — це рядок символів, який потрібно записати у текстовий файл. Цей рядок буде набором рядків, що закінчуються символом кінця рядка \n;
  • рядок 102: функція PHP [file_puts_contents] записує рядок символів у текстовий файл. Вона відкриває файл, записує в нього рядок і закриває файл. У разі виникнення помилки вона повертає логічне значення FALSE;
  • рядок 103: якщо сталася помилка, генерується виняток;
  • рядки 92–96: реалізація методу [saveResults] інтерфейсу [InterfaceDao]. Знову використовується приватний метод [saveString]. Тут другим параметром методу [saveString] є рядок, побудований на основі масиву [$taxPayersData], елементи якого мають тип [TaxPayerData]. Можна задатися питанням, яким буде результат цієї операції:

implode("\n", $taxPayersData)

У класі [TaxPayerData] (параграф «посилання») ми визначили такий метод [__toString]:


public function __toString() {
    // рядок JSON об’єкта
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

Операція


implode("\n", $taxPayersData)

об'єднає кожен елемент масиву [$taxPayersData], перетворений у рядок за допомогою його методу [__toString], із символом кінця рядка \n. У результаті отримаємо рядок такого вигляду:

json1\njson2\n…

Висновок

Клас [TraitDao] реалізував два методи інтерфейсу [InterfaceDao], а саме [getTaxPayersData] та [saveResults]:


<?php

// простір імен
namespace Application;

interface InterfaceDao {

  // читання даних про платників податків
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // читання даних податкової адміністрації (податкові ставки)
  public function getTaxAdminData(): TaxAdminData;

  // запис результатів
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

Нам залишається реалізувати метод [getTaxAdminData], який отримує дані з податкової адміністрації.

11.3.3. Клас [ImpotsWithTaxAdminDataInJsonFile]

Клас [ImpotsWithTaxAdminDataInJsonFile] реалізує інтерфейс [InterfaceDao] наступним чином:


<?php

// простір імен
namespace Application;

// визначення класу ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
  // використання риски
  use TraitDao;
  // об’єкт типу TaxAdminData, що містить дані про податкові групи
  private $taxAdminData;

  // конструктор
  public function __construct(string $taxAdminDataFilename) {
    // потрібно ініціалізувати атрибут [$this->taxAdminData]
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
  }

  // повертає дані, необхідні для розрахунку податку
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }
}

Коментарі

  • рядок 7: клас [ImpotsWithTaxAdminDataInJsonFile] реалізує інтерфейс [InterfaceDao];
  • рядок 9: клас [ImpotsWithTaxAdminDataInJsonFile] використовує рис [traitDao], який, як відомо, реалізує методи [getTaxPayersData] та [saveResults]інтерфейсу [InterfaceDao]. Отже, класу [ImpotsWithTaxAdminDataInJsonFile] залишається лише реалізувати метод [getTaxAdminData], який отримує дані від податкової адміністрації;
  • рядок 11: атрибут типу [TaxAdminData], який повертає метод [getTaxAdminData] з рядків 20–22. Цей атрибут ініціалізується конструктором з рядків 14–17;

Ми завершили роботу з рівнем [dao] нашого додатка: у нас є клас, який повністю реалізує інтерфейс [InterfaceDao], який ми для себе визначили. Тепер ми можемо перейти до рівня [métier].

11.4. Рівень [métier]

Тепер ми реалізуємо рівень [2] нашої архітектури:

Image

11.4.1. Інтерфейс [InterfaceMétier]

Інтерфейс шару [métier] матиме такий вигляд:


<?php

// простір імен
namespace Application;

interface InterfaceMetier {

  // розрахунок податків платника податків
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // розрахунок податків у пакетному режимі
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}

Коментарі

  • рядок 9: інтерфейс [InterfaceMétier] вміє обчислювати суму податку для фізичної особи-платника податків за умови надання йому таких даних: сімейний стан, кількість дітей, річний дохід. Метод [calculerImpot] не використовує шар [dao], тому він не генерує винятків;
  • рядок 9: інтерфейс [InterfaceMétier] також може обчислити суму податку для групи платників податків, дані про яких зібрані у текстовому файлі з назвою [$taxPayersFileName]. Він записує результати у текстовий файл з назвою [$resultsFileName]. Метод [executeBatchImpots] повинен звертатися до рівня [dao], який відповідає за доступ до файлової системи. У цьому випадку з рівня [dao] можуть надходити винятки, які метод [executeBatchImpots] не перехоплюватиме: він дозволить їм передаватися до головного скрипта. Некритичні помилки записуються у текстовий файл із назвою [$errorsFileName];
  • рядок 9: метод [calculerImpot] є суто методом [métier]. Він не зважає на те, звідки походять дані, які він використовує;
  • рядок 12: метод [executeBatchImpots] звертається до рівня [dao] для читання та запису даних у текстові файли. Він неодноразово викликає бізнес-метод [calculerImpot];

11.4.2. Клас [Metier]

Клас [Metier] реалізує інтерфейс [InterfaceMetier] наступним чином:


<?php

// простір імен
namespace Application;

class Metier implements InterfaceMetier {
  // рівень Dao
  private $dao;
  // дані податкової адміністрації
  private $taxAdminData;

  //---------------------------------------------
  // сетер шару [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceDao $dao) {
    // зберігається посилання на рівень [dao]
    $this->dao = $dao;
    // отримуються дані, необхідні для розрахунку податку
    // метод [getTaxAdminData] може викликати виняток ExceptionImpots
    // тоді виняток передається до коду, що викликає
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// розрахунок податку
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // результат
    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 {

    // результат
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=річназарплата-пільга
  // податкова пільга має мінімальне та максимальне значення
  private function getRevenuImposable(float $salaire): float {

    // результат
    return floor($revenuImposable);
  }

// розраховує можливе зниження
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // результат
    return ceil($décôte);
  }

// розраховує можливе зниження
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {

    // результат
    return ceil($réduction);
  }

  // розрахунок податків у пакетному режимі
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {

    // запис результатів
    $this->dao->saveResults($resultsFileName, $results);
  }

}

Коментарі

  • рядок 6: клас [Metier] реалізує інтерфейс [InterfaceMetier], тобто методи [calculerImpot] (рядки 30–34) та [executeBatchImpots] (рядки 66–70);
  • рядок 8: посилання на шар [dao]. Він обов’язково потрібен, щоб шар [métier] знав, до кого звертатися, коли йому потрібні зовнішні дані. Цей атрибут буде ініціалізовано за допомогою сеттера у рядках 14–17 або за допомогою конструктора у рядках 19–26;
  • рядок 10: об’єкт типу [TaxAdminData], який інкапсулює дані податкової адміністрації. Ці дані необхідні для бізнес-методу [calculerImpot]. Цей атрибут ініціалізується за допомогою конструктора в рядках 19–26;
  • рядки 19–26: конструктор ініціалізує обидва атрибути класу:
    • атрибут [$dao] ініціалізується за допомогою посилання, переданого як параметр конструктору. Слід зазначити, що тип цього параметра відповідає типу інтерфейсу [InterfaceDao], що дозволяє ініціалізувати клас [Metier] будь-яким класом, який реалізує цей інтерфейс;
    • атрибут [$taxAdminData] ініціалізується за допомогою виклику методу [getTaxAdminData] з рівня [dao];

З цього випливає, що під час виконання методів [calculerImpots] та [executeBatchImpots] ініціалізуються обидва атрибути [$dao] та [$taxAdminData].

Метод [calculerImpots] має такий вигляд:


public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié: так, ні
    // $enfants: кількість дітей
    // $salaire: річний оклад
    // $this->taxAdminData: дані податкової служби
    //
    // перевіряється наявність даних податкової служби
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // розрахунок податку з урахуванням дітей
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // розрахунок податку без урахування дітей
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // застосування обмеження сімейного коефіцієнта
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART євро за перших двох дітей
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART євро за перших двох дітей, удвічі більше за наступних
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // береться найвищий податок
    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"];
    }
    // розрахунок можливої знижки
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // розрахунок можливого зменшення податку
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // результат
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

Коментарі

  • цей код належить методу [AbstractBaseImpots::calculerImpot] версії 3, описаному в розділі «Посилання». Те саме стосується приватних методів [calculerImpot2, getDecôte, getRéduction, getRevenuImposable];

Метод [Metier::executeBatchImpots] має такий вигляд:


public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // дозволяє передавати винятки, що походять з рівня [dao]
    // отримуємо дані про платників податків
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // таблиця результатів
    $results = [];
    // здійснюється їх аналіз
    foreach ($taxPayersData as $taxPayerData) {
      // розраховується податок
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // заповнюємо [$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"]);
      // заносимо результат у таблицю результатів
      $results [] = $taxPayerData;
    }
    // запис результатів
    $this->dao->saveResults($resultsFileName, $results);
  }

Коментарі

  • рядок 1: метод повинен повторно викликати метод [calculerImpot] для кожного платника податків, знайденого у текстовому файлі з назвою [$taxPayersFileName]. Він повинен записати результати у текстовий файл з назвою [$resultsFileName]. Виявлені некритичні помилки записуються у текстовий файл із назвою [$errorsFileName]. Метод сам не генерує винятків, але пропускає ті, що генерує шар [dao];
  • рядок 4: дані платників податків запитуються у шарі [dao]. Цей шар повертає масив елементів типу [TaxPayerData], який є класом атрибутів [marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux] (див. параграф «посилання»). Якщо тут виникає виняток, оскільки він не перехоплюється catch, він автоматично передається до коду, що викликає. Це означає, що в разі винятку рядок 6 не виконується;
  • рядок 6: масив результатів типу [TaxPayerData];
  • рядки 8–22: обчислюється податок для кожного елемента масиву платників податків [$taxPayersData]. Для цього викликається внутрішній метод [calculerImpot] (рядок 10);
  • рядки 15–19: отриманий результат використовується для ініціалізації атрибутів таблиці [TaxPayerData], які ще не були ініціалізовані;
  • рядок 21: отриманий результат накопичується в таблиці результатів [$results];
  • рядок 24: після розрахунку податку для всіх платників податків результати зберігаються у текстовому файлі. Цю роботу виконує шар [dao];

Висновок

Загалом шар [métier] досить простий у написанні, оскільки він звертається до шару [dao], який, у свою чергу, керує доступом до даних та відповідним обробленням помилок.

11.5. Головний скрипт

Тепер напишемо скрипт шару [3] нашої архітектури:

Image

Головний скрипт має такий вигляд: [main.php]:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// обробка помилок за допомогою PHP
//ini_set("display_errors", "0");

// включення інтерфейсів та класів
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";
// тест -----------------------------------------------------
// визначення констант
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // створення шару [dao]
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // створення шару [métier]
  $métier = new Metier($dao);
  // розрахунок податків у пакетному режимі
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // виведення повідомлення про помилку
  print $ex->getMessage() . "\n";
}
// кінець
print "Terminé\n";
exit;


Коментарі

  • рядок 24: ім’я файлу даних платників податків;
  • рядок 25: ім’я файлу результатів;
  • рядок 26: ім’я файлу з помилками;
  • рядок 27: ім’я файлу jSON, що містить дані податкової адміністрації;
  • рядок 31: створення шару [dao];
  • рядок 33: створення шару [métier] на основі цього шару [dao];
  • рядок 35: виконання методу [executeBatchImpots] шару [métier];
  • рядки 36–39: як ми вже бачили, шар [métier] може генерувати винятки. Вони перехоплюються саме тут;

11.6. Візуальні тести

11.6.1. Тест № 1

З файлом платників податків [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

отримуємо такий файл помилок [errors.txt]:


la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée

та файл результатів [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. Тест № 2

У головному скрипті для файлу платників податків вказуємо ім’я файлу, якого не існує:

const TAXPAYERS_DATA_FILENAME = "taxpayersdata2.txt";

Результати, отримані в консолі, такі:


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.
  • рядок 1: попередження (warning) інтерпретатора PHP;
  • рядок 2: повідомлення про помилку винятку, згенерованого рівнем [dao];

Можна приглушити повідомлення про помилки інтерпретатора PHP:

Image

У рядку 21 наведеного вище коду вказано, що помилки PHP не повинні відображатися. На етапі розробки їх необхідно відображати. У виробничому режимі їх слід приховати.

Результати виконання будуть такими:


Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé

11.7. Тести [Codeception]

Візуальні тести є вкрай недостатніми:

  • зазвичай обмежуються лише кількома тестами;
  • під час такої візуальної перевірки ми не завжди достатньо уважні, і деякі деталі можуть вислизнути з-під нашої уваги;

У реальності професійної розробки тести пишуть спеціалісти, для яких це є основною функцією. Вони прагнуть зробити тести якомога повнішими. Для цього вони використовують фреймворки для тестування.

Тут ми будемо використовувати фреймворк Codeception [https://codeception.com/], оскільки його можна інтегрувати в NetBeans. Це фреймворк із широким спектром можливостей. Ми скористаємося лише деякими з них. Ідея полягає в тому, щоб мати швидкий спосіб перевіряти працездатність кожної нової версії програми після її створення. Наявність успішно виконаних тестів надає розробнику впевненості в написаному ним коді. Це важливий фактор.

11.7.1. Встановлення фреймворку [Codeception]

Як і багато бібліотек PHP, фреймворк [Codeception] встановлюється разом із [Composer]. Тому ми відкриваємо термінал Laragon (див. параграф із посиланням).

Спочатку нам потрібно встановити фреймворк для тестування PHPUnit [https://phpunit.de/]. Адже Codeception використовує у фоновому режимі фреймворк PHPUnit:

Image

Далі встановлюємо фреймворк Codeception:

Image

Ось і все. Тепер розглянемо інтеграцію [Codeception] у NetBeans.

11.7.2. Інтеграція [CodeCeption] у NetBeans

Image

  • у [1-2] відкриваємо властивості проєкту;
  • у [3-4] ми робимо [Codeception] одним із тестових фреймворків проєкту;

Image

Image

  • у [5-8] ініціалізується фреймворк [Codeception] для проекту;

Image

  • у [9] було створено папку [tests], а також файл конфігурації [codeception.yml] у [10-11]. Файл [11] є таким самим, як і файл [10]. Codeception просто створив папку [Important Files], щоб надати особливого значення файлу [10];
  • у файлі [12-13] ми повертаємося до властивостей проєкту;

Image

  • у [14-16] папка [tests] [16] позначається як папка тестів проєкту;
  • у [16] папка [tests] з’являється під новою назвою [Test Files]. Наявність цієї папки в проєкті PHP свідчить про те, що цей проєкт містить фреймворк для програмованих тестів;
  • ми створимо наші тести в папці [unit] [17];

11.7.3. Тести рівня [dao]

Image

  • ми створимо всі наші тести у папці [unit] [1];
  • імена тестових класів [Codeception] повинні закінчуватися ключовим словом [Test], інакше класи не будуть розпізнані як тестові;

Наші тестові класи [Codeception] матимуть такий вигляд: [https://codeception.com/docs/05-UnitTests]:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// завантаження тестового середовища


class DaoTest extends \Codeception\Test\Unit {
  // атрибути тесту
  private $attribut1;

  public function __construct() {
    parent::__construct();
    // ініціалізація тестового середовища

  }

  // тести
  public function testTaxAdminData() {
    // тести
    $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);

  }

}

Коментарі

  • рядок 7: тестові класи будуть у тому самому просторі імен, що й тестована програма;
  • рядки 9–10: тут містяться операції [require] для завантаження тестованих класів та інтерфейсів;
  • рядок 12: назва тестового класу обов’язково має закінчуватися ключовим словом [Test]. Цей клас має успадковувати клас [\Codeception\Test\Unit];
  • рядки 16–20: конструктор дозволить нам ініціалізувати тестове середовище;
  • рядок 23: імена тестових методів обов’язково повинні починатися з ключового слова [test];
  • рядки 25–31: можна використовувати різні методи тестування;

Клас тесту [DaoTest] матиме такий вигляд:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// константи
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// включення інтерфейсів та класів
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";;
// тест -----------------------------------------------------
// визначення констант
const TAXADMINDATA_FILENAME = "taxadmindata.json";

class DaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    parent::__construct();
    // створення шару [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // тестування
  public function testTaxAdminData() {

  }
}

Коментарі

Для створення тестів для певної версії практичного завдання ми будемо використовувати середовище, ідентичне тому, що використовується головним скриптом цієї версії. Для версії 04 це наступний скрипт [main.php]:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// обробка помилок за допомогою PHP
ini_set("display_errors", "0");

// підключення інтерфейсів та класів
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";
// тест -----------------------------------------------------
// визначення констант
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // створення шару [dao]
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // створення шару [métier]
  $métier = new Metier($dao);
  // розрахунок податків у пакетному режимі
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // виведення повідомлення про помилку
  print $ex->getMessage() . "\n";
}
// кінець
print "Terminé\n";
exit;

Щоб протестувати шар [dao], у класі тестування:

  • використовуємо середовище з рядків 13–27 скрипта [main.php];
  • у конструкторі тестового класу створюємо шар [dao], як у рядку 31;
  • пишемо методи тестування;

Ми будемо діяти таким чином для всіх тестових класів.

Повернемося до повного коду тестового класу:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// константи
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// імпорт інтерфейсів та класів
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";;
// тест -----------------------------------------------------
// визначення констант
const TAXADMINDATA_FILENAME = "taxadmindata.json";

class DaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    parent::__construct();
    // створення шару [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // тестування
  public function testTaxAdminData() {
    // константи розрахунку
    $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());
    // податкові ставки
    $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());
  }

}

Коментарі

  • рядки 10–25: завантаження середовища, необхідного для тестування, та визначення констант;
  • рядки 31–36: побудова шару [dao] (рядок 34), а потім ініціалізація атрибута [$taxAdminData] (рядок 29). Цей атрибут містить дані податкової адміністрації;
  • рядки 39–55: єдиний метод тестування. Він полягає у перевірці того, чи вміст атрибута [$taxAdminData] відповідає очікуваним значенням;
  • рядки 41–50: перевірка констант розрахунку податку;
  • рядки 52–55: перевірка податкових шкал. Метод [assertSame] перевіряє, чи два об’єкти PHP, у даному випадку таблиці, є ідентичними;

Щоб виконати цей клас тесту, слід діяти наступним чином:

Image

  • у [1-2] виконується тест;
  • [3]: вікно результатів тестування;
  • [4]: виконаний тестовий клас;
  • [5]: результати. Тут єдиний метод тестування пройшов успішно;
  • [6]: коли тест завершується з помилкою або, що трапляється частіше, коли жоден тест не було виконано, слід перейти до вікна [6]. Найчастіше саме завантаження тестового середовища завершилося з помилкою, і тому жоден тест не зміг бути виконаний. Помилки, що відображаються у вікні [6], — це ті, які виникають під час виконання звичайного скрипта PHP;

Розглянемо приклад тесту, що завершився помилкою:

У класі тесту ми вносимо помилку в визначення константи:


// константи
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");

потім виконуємо тест. Отриманий результат такий:

Image

У вікні [4]:

Image

11.7.4. Тестування шару [métier]

Клас тесту [MetierTest] побудовано за тими самими правилами, що й клас [DaoTest], але він містить більше методів тестування:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// константи
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// включення інтерфейсів та класів
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";;
// тест -----------------------------------------------------
// визначення констант
const TAXADMINDATA_FILENAME = "taxadmindata.json";

class MetierTest extends \Codeception\Test\Unit {
  // бізнес-шар
  private $métier;

  public function __construct() {
    parent::__construct();
    // створення шару [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    // створення шару [métier]
    $this->métier = new Metier($dao);
  }

  // тестування
  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"]);
  }
}

Коментарі

  • рядки 10–25: завантаження файлів, що визначають тестове середовище. Воно таке саме, як і для шару [dao];
  • рядки 31–37: створення екземплярів шарів [dao] та [métier];
  • рядки 40–47: тест на розрахунок податку;
  • рядок 41: певний розрахунок податку виконується за допомогою шару [métier];
  • рядки 42–46: перевіряється, чи отримані результати збігаються з результатами симулятора податкової адміністрації [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm];
  • рядки 23–26: тести на рівність проводяться з точністю до 1 євро. Адже було виявлено, що через проблеми з округленням алгоритм, описаний у документі, давав очікувані результати з точністю до 1 євро;
  • рядок 27: ставка оподаткування розраховується без похибки;
  • рядки 49–137: такі тести повторюються 10 разів, щоразу з різними параметрами платника податків;

Тести дають такі результати:

Image

11.7.5. Тестування наступних версій

Надалі тестування шарів [dao] та [métier] буде ідентичним тестуванню версії 04. Зміниться лише тестове середовище. Тому ми наведемо лише його опис та результати тестування.