Hlavní navigace

Jednotkové testování v PHP s knihovnou PHPUnit

3. 11. 2021
Doba čtení: 12 minut

Sdílet

 Autor: Depositphotos
V první části budou vysvětleny základní pojmy včetně výběru knihovny PHPUnit. Dozvíme se jaké jsou základní části jednotkového testu, jak začlenit testy do prostředí PhpStorm a jak pracovat s výsledky testů.

Poznámka: Články budou popisovat základní použití uvedených technik bez ambicí o úplnost. Pokud máte zkušenosti s různými rozšířeními, jinými frameworky, jinými způsoby používání, zmiňte je, prosím, v diskuzi. Naším společným cílem by mělo být rozšíření poznání čtenářů Root.cz.

Terminologická poznámka: Místo termínu jednotkové testování se někdy používá termín testování komponent. Je to například v ISTQB studijních materiálech. A nutno říci, že termín testování komponent je významově přesnější než termín jednotkové testování. Na druhou stranu je ale termín jednotkové testování mnohem známější.

1. Užitečné odkazy

2. Úvodní informace a použitá knihovna

Průběžné používání jednotkových testů by měla být pro dobrého programátora stejná samozřejmost jako průběžné psaní programátorské dokumentace. Psaní tohoto typu testů se považuje za vývojářskou (nikoliv testerskou) aktivitu a má se za to, že dobře napsaná sada těchto testů znamená poloviční úspěch celého vývoje.

Tento způsob testování je také nejlevnější, protože testy se píší a testování probíhá souběžně s vytvářením kódu, tzn. na chybu se přijde okamžitě a její oprava je velmi laciná. Vše totiž provádí jeden člověk – programátor. Další významnou sekundární výhodou je, že pokud programátor používá při vývoji souběžně jednotkové testy, nutí jej tento způsob práce, aby produkoval zdrojový kód, který je dobře testovatelný. To představuje vytváření malých (nikoliv rozsáhlých) metod, s jednoznačným účelem, s jasně definovaným rozhraním (kontraktem), co nejméně provázané s ostatním kódem atp. Toto všechno je v souladu s doporučeními ohledně zvyšování kvality kódu a na ně navázaných metrik.

Prostě jen velmi těžko najdete na používání jednotkových testů nějakou nevýhodu, kromě té, že z počátku jejich používání představují malou časovou investici navíc. Ta se ale brzy vrátí.

Základem pro psaní jednotkových (unit) testů je knihovna xUnit. Tvůrce konceptu jednotkového testování Kent Beck ji připravil nejprve pro Smalltalk. Tento koncept se ukázal natolik kvalitní / výhodný / využitelný, že existuje ve variantách pro různé programovací jazyky. Pro PHP existuje PHPUnit, přičemž druhý rozšířený framework je Codeception.

Cílem tohoto článku nebude jejich vzájemné porovnání. Na základě porovnání již provedeného (září 2021) byl pro další práci vybrán PHPUnit.


Autor: Pavel Herout

PHPUnit je velmi rozšířený způsob psaní a spouštění testů, v podstatě se dá považovat za de facto průmyslový standard. V současné době je součástí většiny RAD (PhpStorm, Eclipse, …), které poskytují testům všemožnou podporu. Ta spočívá např. v automatickém generování kostry testů, grafické nadstavbě pro jejich spouštění a vizualizaci výsledků, možnosti hromadného spouštění testů apod.

Podíváme-li se do nejbližší historie, vidíme, že tvůrce PHPUnit pan Sebastian Bergmann má ve zvyku vydávat hlavní verze vždy začátkem února:

  • 7.0 – 2.2.2018
  • 8.0 – 1.2.2019
  • 9.0 – 7.2.2020

Poznámka: V dalších příkladech bude používána verze 9.5.4, která má logo:


PHPUnit je distribuován běžnými způsoby – Composer, atp. V dále uvedených příkladech bude použito „ruční“ nastavení za významné podpory PhpStorm, kdy se stahuje soubor phpunit.phar (4,4 MB).

Varování: Zde popisované možnosti PHPUnit jsou jen subjektivním výběrem těch nejužitečnějších. Ve skutečnosti jsou možnosti PHPUnit větší. Pokud zde není popsáno řešení problému, kterému čelíte, hledejte v dokumentaci PHPUnit.

3. Ukázka jednoduché entitní třídy pro testování

Abychom mohli začít testovat, potřebujeme k tomu testovaný kód. V této části bude ukázán příklad takovéhoto jednoduchého testovaného kódu.

Třída HodnoceniPredmetu slouží pro záznam známky z předmětu. Je napsaná velmi zjednodušeně, ale obsahuje všechny potřebné části pro ukázky základů jednotkového testování.

Budeme-li vytvářet její instance, lze vytvořit instanci pouze s uvedeným názvem předmětu, tj. bez jeho hodnocení – pak je známka nastavena na hodnotu DOSUD_NEHODNOCENO. V tomto případě vrací metoda isNehodnoceno() hodnotu true.

Metoda setZnamka(int znamka) nejprve ověřuje, zda je nastavovaná známka v povoleném rozsahu. Pokud ne, je vyhozena run-time výjimka \InvalidArgumentException.

<?php
namespace app\zaklad;

class HodnoceniPredmetu
{
  // konstanty pro známkování
  const VYBORNE = 1;
  const NEDOSTATECNE = 5;
  const DOSUD_NEHODNOCENO = 0;
  const DOSUD_NEHODNOCENO_SLOVY = "N/A";

  private $nazev;
  private $znamka;

  public function __construct(string $nazev, int $znamka=self::DOSUD_NEHODNOCENO)
  {
    $this->nazev = $nazev;
    $this->setZnamka($znamka);
  }

  public function getNazev() : string
  {
    return $this->nazev;
  }

  public function getZnamka() : int
  {
    return $this->znamka;
  }

  /**
   * Nastavuje známku v rozsahu od VYBORNE do NEDOSTATECNE
   * @param znamka nastavovaná známka
   * @throws InvalidArgumentException pokud je známka mimo rozsah
   */
  public function setZnamka(int $znamka) : void
  {
    if ($znamka >= self::VYBORNE  &&  $znamka <= self::NEDOSTATECNE) {
      $this->znamka = $znamka;
    }
    elseif ($this->znamka === null  &&  $znamka == self::DOSUD_NEHODNOCENO) {
      // prvni nastaveni v konstruktoru
      $this->znamka = $znamka;
    }
    else {
      throw new \InvalidArgumentException($znamka);
    }
  }

  /**
   * Zjistí, zda je předmět dosud nehodnocen
   * @return true, pokud je předmět nehodnocen
   */
  public function isNehodnoceno() : bool
  {
    return ($this->znamka == self::DOSUD_NEHODNOCENO);
  }

  public function __toString() : string
  {
    if ($this->isNehodnoceno() == true) {
      return $this->nazev . ": " . self::DOSUD_NEHODNOCENO_SLOVY . "<br>";
    }
    else {
      return $this->nazev . ": " . $this->znamka . "<br>";
    }
  }
}

3.1. „Klasické“ ověření funkčnosti

Termín „klasické“ je zde použit ve významu „jak by postupoval programátor třídy HodnoceniPredmetu při neznalosti jednotkového testování“. Tato část zde bude uvedena pro budoucí srovnání s principem jednotkového testování.

Máme-li testovanou třídu (termín pro ni by byl SUT – System Under Test), můžeme napsat pokusný index.php, který ji bude ověřovat:

<?php
require_once("autoload.php");
use app\zaklad\HodnoceniPredmetu;

$hodnoceni = new HodnoceniPredmetu("Fyzika", 1);
echo $hodnoceni->__toString();

$predmet = new HodnoceniPredmetu("Matika");
echo $predmet->__toString();
echo "nehodnoceno:" . $predmet->isNehodnoceno() . "<br>";
$predmet->setZnamka(3);
echo $predmet->__toString();

Přičemž struktura projektu je:


Autor: Pavel Herout

Obsah souboru autoload.php. BTW: Zde není autoload nezbytný. Je uveden proto, že tentýž soubor bude používán i ve všech dalších příkladech:

<?php
set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__ . '\\src');
spl_autoload_extensions('.class.php,.trait.php,.inc.php,.php');
spl_autoload_register();

Po spuštění index.php dostaneme:


Autor: Pavel Herout

To, zda je výsledek očekávaný či nikoliv, musíme ověřit pohledem a případně i nějakou další analýzou kódu. Zde se jedná např. o výsledek nehodnoceno:1 , který ve skutečnosti znamená nehodnoceno:true

Při použití tohoto postupu lze opravdu najít všechny zásadní problémy a hned je opravit. ALE! Do budoucna je zcela ztraceno veškeré úsilí, které programátor do tohoto ověřování vložil. Pod pojmem „úsilí“ se myslí jak psaní konkrétních výkonných částí kódu, tak i mentální vyhodnocování, zda ověřování prokázalo správnou či nesprávnou funkci programu.

Místo tohoto postupu můžeme začít psát regulérní testovací případy, které poslouží lépe a navíc budou moci být trvalým doplňkem této třídy. Nad nejasnými případy nám bude stačit přemýšlet pouze jednou při prvním spuštění, vyhodnocení všech ostatních spuštění již provede počítač, respektive použitá knihovna testů.

4. Použití testů s využitím PHPUnit v PhpStorm

Poznámka: Příklad je ukazován na PhpStorm ve verzi 2021.1.

Poznámka: PHPUnit lze samozřejmě používat i bez podpory jakéhokoliv RAD. Na stránkách PHPUnit Take the first steps / Getting Started si můžete přečíst, jak na to.

4.1. Příprava adresáře test

Je dobrým zvykem oddělit kód testů od výkonného kódu. To umožňuje v projektu snadněji udržovat pořádek a při nasazení do produkce nenasazovat i testy.


Autor: Pavel Herout

Pomocí Mark Directory as z kontextového menu se označí jako testovací.


Autor: Pavel Herout

Výsledek, kdy je adresářtest podbarven zeleně, což je standardní barva pro testy:


Autor: Pavel Herout

4.2. Vygenerování kostry testovací třídy

Příprava kostry třídy testovacích případů a následné spouštění testů je v PhpStorm maximálně zautomatizováno.
V editoru otevřeme třídu HodnoceniPredmetu a pomocí kontextového menu Code / Generate … zvolíme Test


Autor: Pavel Herout

Zde postupně:

  • Vybereme šablonu PHPUnit
  • Doplníme podadresář, kam bude soubor testu uložen, tj. \app\zaklad
    Z hlediska udržení pořádku je vhodné mít testy ve stejné adresářové struktuře, jako je testovaný kód
  • Zaškrtneme setZnamka, tj. metodu, pro kterou chceme vygenerovat test
  • Všechna ostatní nastavení PhpStorm inteligentně vyplní, včetně jména nové testovací třídy. Testovací případy jsou typicky uloženy ve třídě, která se jmenuje TestovanáTřídaTest, zde  HodnoceniPredmetuTest

Autor: Pavel Herout

Výsledek bude:


Autor: Pavel Herout

4.3. Doplnění zdrojového kódu testu

Do předpřipravené kostry metody testSetZnamka() doplníme tentýž kód (bez příkazů echo), kterým jsme zkoušeli funkčnost třídy v index.php, tj.:

    $predmet = new HodnoceniPredmetu("Matika");
    $predmet->setZnamka(3);

PhpStorm nám nabídne doplnění:
use app\zaklad\HodnoceniPredmetu;
Ovšem pokud jsou testy ve stejném jmenném prostoru, je toto doplnění zbytečné.

Jako poslední příkaz zavoláme metodu pro automatické vyhodnocení testu, která je ze zděděného TestCase:
$this->assertEquals(3, $predmet->getZnamka());

Výsledek bude:


Autor: Pavel Herout

Nyní je připraven testovací případ a je nutné provést konfiguraci testů.

4.4. Natažení a instalace PHPUnit

Spustíme testy ze třídy HodnoceniPredmetuTest (zelenou šipkou vlevo od zdrojového kódu), přičemž v pravém horním rohu je výpis, že čeká na konfiguraci.


Autor: Pavel Herout

Pokus o spuštění dosud nenakonfigurované knihovny PHPUnit dle očekávání selže:


Autor: Pavel Herout

Ale je již předpřipravena konfigurace pro spuštění testu:


Autor: Pavel Herout

Pomocí File / Settings vybereme typ testovacího frameworku PHPUnit Local:


Autor: Pavel Herout

Zvolíme, že budeme používat lokální verziphpunit.phar a následně se objeví hlášení, že není instalován, ovšem společně s odkazem na automatický download.


Autor: Pavel Herout

Po kliknutí na download musíme určit, do jakého adresáře budephpunit.phar uložen. Adresář je předpřipravený, stačí jen odkliknout.

Pokud s PHPUnit začínáme (což je náš případ) je výhodné phpunit.phar uložit do přednastaveného adresáře projektu, protože PhpStorm nám bude moci z něj našeptávat nápovědu. Nevýhodou ale je, že pro víc projektů bychom museli mít na disku víc kopií phpunit.phar.
Takže až budeme PHPUnit rutinně ovládat a našeptávání nebudeme potřebovat, lze phpunit.phar uložit na jedno místo, odkud bude sdílen všemi projekty.


Autor: Pavel Herout

Pomocí automatického downloadu se stahuje nejnovější verze PHPUnit, takže při svých pokusech pravděpodobně uvidíte vyšší verzi.


Autor: Pavel Herout

Poslední nutnou akcí je zadání, kde je uložen autoloader, zde označovaný jako bootstrap:


Autor: Pavel Herout

Zvolíme náš autoload.php :


Autor: Pavel Herout

Výsledná konfigurace frameworku je: (Nezapomeneme závěrečně odkliknout OK.)


Autor: Pavel Herout

4.5. Skutečné spuštění testů

Když stejným způsobem spustíme testy, dostaneme:


Autor: Pavel Herout

5. Části testovacího případu

Vygenerovaná a námi doplněná testovací třída HodnoceniPredmetuTest obsahuje některé dosud neznámé příkazy:

<?php

namespace app\zaklad;

use app\zaklad\HodnoceniPredmetu;
use PHPUnit\Framework\TestCase;

class HodnoceniPredmetuTest extends TestCase
{
  public function testSetZnamka()
  {
    $predmet = new HodnoceniPredmetu("Matika");
    $predmet->setZnamka(3);
    $this->assertEquals(3, $predmet->getZnamka());
//    self::assertEquals(3, $predmet->getZnamka());
  }
}

Poznámka:
Zdrojové kódy xUnit testů často obsahují „magická čísla“ (zde 3), což není považováno za nedokonalost kódu.

Význam jednotlivých částí kódu:

use PHPUnit\Framework\TestCase;
– umožní používat třídu TestCase  – viz dále
extends TestCase
– naše třída testů dědí od třídy TestCase, což umožní používat všechny její metody a konstrukce, např.  $this->assertEquals()
public function testSetZnamka()
– hlavička metody, která představuje jeden testovací případ; může být typu void, tj.:
public function testSetZnamka() : void
Název metody je odvozen od názvu testované metody setZnamka a musí začínat slovem test. Podle tohoto identifikátoru PHPUnit pozná, že se jedná o testovací metodu, kterou má spustit.
Jméno metody často končí pojmenováním toho, co se testuje (zde nepoužito), případně pořadovým číslem testovacího případu (viz dále). Je běžné, že jedna metoda je testována více testovacími případy – pozitivní a negativní testy. V tomto uvedeném příkladu se používá jeden pozitivní test.
V těle metody by měla být alespoň jedna – typicky právě jedna – metoda assert... Toto pravidlo se občas rozumně porušuje, dále uvidíme více assert metod v jednom testovacím případě.
$predmet = new HodnoceniPredmetu("Matika");
– vytvoření objektu testované třídy – běžný Php kód
$predmet->setZnamka(3);
– nastavení známky – běžný Php kód
$this->assertEquals(3, $predmet->getZnamka());
– vlastní test s využitím metody assertEquals() ze třídy TestCase z PHPUnit.
Zde platí, že první skutečný parametr je očekávaná hodnota a druhý skutečný parametr je aktuální hodnota objektu predmet. Pokud se budou hodnoty obou parametrů rovnat, tento testovací případ proběhne úspěšně.
Poznámka:
Testy píšeme tak, aby prošly, tj. tak, jak očekáváme, že bude program správně fungovat. Toto pravidlo platí pro pozitivní i negativní testy.

5.1. Pravidlo Arange – Act – Assert

Test by měl být napsán tak, aby obsahoval tři části:

  1. Arange – nastavení testovaného objektu do stavu, v němž bude testován; často je to vytvoření tohoto objektu
      $predmet = new HodnoceniPredmetu("Matika");
  2. Act – provedení testované aktivity, tj. nejčastěji volání prověřované metody
      $predmet->setZnamka(3);
  3. Assert – porovnání očekávaného stavu testovaného objektu se stavem skutečným
      $this->assertEquals(3, $predmet->getZnamka());

Tyto tři části by měly být co nejjednodušší, ideálně vždy jen jedna řádka kódu.

6. PHPUnit test odhalil chybu

Jedná se o neúspěšný průběh testu, kdy test selhal.
Platí zásada, že pokud v celé testovací třídě alespoň jeden testovací případ selhal (zde v ukázce je zatím pouze jeden selhaný TC), je celý test chybně.

Selhaný testovací případ je ohlášen chybovou zprávou a tato zpráva je pak vypsána způsobem závislým na RAD.

Například pro metodu, ve které je změněna očekávaná hodnota na 2

    public function testSetZnamka()
    {
      $predmet = new HodnoceniPredmetu("Matika");
      $predmet->setZnamka(3);
      $this->assertEquals(2, $predmet->getZnamka());
    }

Po spuštění dostaneme červený výsledek testu a vidíme důvod selhání testu:


Autor: Pavel Herout

Je možné kliknout i na podrobnější zobrazení rozdílu, ale v tomto případě nedostaneme oproti textovému výpisu žádnou informaci navíc.


Autor: Pavel Herout

Význam má zobrazení rozdílu např. už u porovnání řetězců (ukázka je z jiného testu).


Autor: Pavel Herout

Důležitější je informace v samotném editoru, kde zčervenají ikony u testů, které selhaly:


Autor: Pavel Herout

6.1. Rozdíl mezi Error a Failure

Častý problém, který je poměrně záludný, je v případě, že testy se jeví jako by selhávaly (tj. objevily chybu v testované třídě), ovšem jedná se o falešné hlášení (false positive).

Celý problém si ukážeme na testovací třídě FailureErrorTest. Ta má dvě testovací metody, přičemž:

  1. metoda testIsNehodnoceno_Failure() očekávaně selže
  2. v metodě testIsNehodnoceno_Error() assert vůbec neproběhne, protože je v ní zapomenuté vytvoření objektu třídy  HodnoceniPredmetu
<?php

namespace app\zaklad;

use app\zaklad\HodnoceniPredmetu;
use PHPUnit\Framework\TestCase;

class FailureErrorTest extends TestCase
{
  public function testIsNehodnoceno_Failure()
  {
    $predmet = new HodnoceniPredmetu("Matika");
    $this->assertFalse($predmet->isNehodnoceno());
  }

  public function testIsNehodnoceno_Error()
  {
    $predmet = null;
    // chybně předpokládáme, že instance HodnoceniPredmetu je již vytvořena
    $this->assertFalse($predmet->isNehodnoceno());
  }
}

Při pozornějším pohledu na výsledky testů v PhpStorm je třeba rozlišovat výsledky typu Failure a Error:

Linux tip

1. Failure – test selže, protože selže metoda  assertXY()


Autor: Pavel Herout

2. Error – test selže, protože je v něm nějaká programová chyba; typický případ je, kdy v těle testu voláme nějakou instanční metodu, ale v metoděsetUp() (viz dále) – nebo někde jinde – nebyla instance vytvořena


Autor: Pavel Herout

Autor článku

Pracuje na Katedře informatiky a výpočetní techniky Fakulty aplikovaných věd na Západočeské univerzitě v Plzni, zabývá se programovacími jazyky, softwarovými technologiemi a testováním.