Hlavní navigace

Jednotkové testování v PHP: parametrizované testy

24. 11. 2021
Doba čtení: 6 minut

Sdílet

 Autor: Depositphotos
V posledním dílu této minisérie budou zmíněny základní možnosti tzv. parametrizovaných testů. Ty jsou výhodné v případě, že chceme tentýž kód testu spustit s různými vstupními daty.

11. Parametrizované testy

Představují důležitou možností PHPUnit, kdy je možné připravit sadu parametrů, nad kterými bude opakovaně spouštěn jeden anotovaný testovací případ. Výhodných případů užití nalezneme postupně celou řadu, např. pro testy mezních hodnot apod.

Od běžných dříve popisovaných testů se liší tím, že se u nich musí vždy specifikovat alespoň jeden datový zdroj. Jednotlivé typy parametrizovaných testů budou v následujícím popisu rozděleny podle typu a charakteru tohoto datového zdroje.

11.1. Malý počet parametrů jednoho typu – @testWith

Má-li test relativně málo parametrů (cca max. 10, přičemž toto omezení je dáno pouze subjektivní čitelností zdrojového kódu testu), je výhodné použít anotaci @testWith.

Tato anotace je (v dokumentačním komentáři) doplněna seznamem parametrů v hranatých závorkách, které jsou předávány do takto anotované testovací metody. Platí pravidlo, že každý parametr musí být na samostatné řádce.
Toto je trochu omezující/nepraktické (z důvodu narůstajícího počtu řádek komentáře) v případě, že parametr je pouze jedna hodnota, tj. není v N-ticích.

Vazba tohoto seznamu parametrů na kód testu se pak provede tím, že testovací metoda má formální parametry, jejichž počet a typ je odvozen od parametrů anotace  @testWith.

Příklad použití bude ukázán na metodě setZnamka() třídy HodnoceniPredmetu

    /**
     * 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);
      }
    }

V příkladě je ukázka otestování platného a neplatného rozsahu známky.
Pozor: Hodnota [1] u testNeplatnyRozsahZnamky()  je záměrná chyba, aby byl vidět způsob selhání testu.

<?php

namespace app\parametrizovane;

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

class TestWithTest extends TestCase
{
  /**
   * @testWith [1]
   *           [2]
   *           [3]
   *           [4]
   *           [5]
   */
  public function testPlatnyRozsahZnamky(int $znamka) : void
  {
    $predmet = new HodnoceniPredmetu("Matika", $znamka);
    $this->assertEquals($znamka, $predmet->getZnamka());
  }

  /**
    * @testWith [-1]
    *           [0]
    *           [1]
    *           [6]
    */
  public function testNeplatnyRozsahZnamky(int $znamka) : void
  {
    $predmet = new HodnoceniPredmetu("Matika");
    $this->expectException(\InvalidArgumentException::class);
    $predmet->setZnamka($znamka);
  }
}

Po spuštění se pro testPlatnyRozsahZnamky() zobrazí:


Autor: Pavel Herout

Pro testNeplatnyRozsahZnamky() s úmyslnou chybou se zobrazí:


Autor: Pavel Herout

11.2. Malý počet parametrů N-tice – @testWith

Pomocí anotace @testWith  je možné definovat libovolnou N-tici parametrů, dále nazývanou element. Jeho omezení je pouze v tom, že každý element musí být pole vyhovující definici v JSON.
Typickým elementem je dvojice [vstupní_hodnota, očekávaný_výstup]. Ovšem snadno si dokážeme představit trojice [vstupní_hodnota, očekávaný_výstup, přesnost] či jiné smysluplné N-tice.

Použití N-tic bude ukázáno na testech triviální testované třídy  NasobilkaPeti.

<?php
namespace app\parametrizovane;

class NasobilkaPeti
{
  public static function vypocti(int $cislo) : int
  {
    return $cislo * 5;
  }
}

V příkladu je ukázka testu malé násobilky opět se záměrnou chybou  [4, 21]

<?php
namespace app\parametrizovane;

use app\parametrizovane\NasobilkaPeti;
use PHPUnit\Framework\TestCase;

class TestWithNticeTest extends TestCase
{
  /**
   * @testWith [1, 5]
   *           [2, 10]
   *           [3, 15]
   *           [4, 21]
   *           [5, 25]
   */
  public function testMalaNasobilka(int $vstup, int $ocekavano) : void
  {
    $this->assertEquals($ocekavano, NasobilkaPeti::vypocti($vstup));
  }
}

Po spuštění se zobrazí:


Autor: Pavel Herout

11.3. Větší počet N-tic parametrů – @dataProvider

Pomocí anotace @dataProvider se lze odkázat na jméno instanční metody vracející jako array seznam (vypočtených/připravených/získaných) parametrů.

Jiným návratovým typem této metody může být místo seznamu hodnot objekt implementující rozhraní Iterator. Součástí dokumentace k PHPUnit je kompletní ukázka třídy implementující toto rozhraní, která umožňuje načíst data z CSV souboru.

Metoda pro přípravu dat nesmí mít žádné formální parametry.
Užitečná věc je, že vracené pole může být klíčované. Hodnota klíče se pak vypisuje jako název datasetu, což zajistí přehlednější (libovolně přehledný) výpis výsledků.

Ukázka „ruční“ přípravy klíčovaného ( '1 * 5' ) elementu jako pole dvou prvků typu int. Opět je zanesena záměrná chyba  '4 * 5' => [4, 21].

Poznámka: Samozřejmě, že o smysluplnosti takovéto „ruční“ přípravy lze diskutovat. Nicméně z mého pohledu je zápis přehlednější, než tomu bylo u seznamu N-tic v dokumentačním komentáři u anotace @testWith. Další možnost využití této „ruční“ přípravy bych viděl pro rychlé prototypování testu, kdy se po ověření nahradí tělo metody skutečným kódem – viz následující část.

<?php
namespace app\parametrizovane;

use app\parametrizovane\NasobilkaPeti;
use PHPUnit\Framework\TestCase;

class DataProviderKlicTest extends TestCase
{
  /**
   * @dataProvider pripravaDat
   */
  public function testMalaNasobilka(int $vstup, int $ocekavano) : void
  {
    $this->assertEquals($ocekavano, NasobilkaPeti::vypocti($vstup));
  }

  public function pripravaDat() : array
  {
    return [
      '1 * 5' => [1, 5],
      '2 * 5' => [2, 10],
      '3 * 5' => [3, 15],
      '4 * 5' => [4, 21],
      '5 * 5' => [5, 25]
    ];
  }
}

Po spuštění se zobrazí:


Autor: Pavel Herout

11.4. N-tice parametrů vytvořené programově – @dataProvider

Výše uvedené použití anotace @dataProvider, kdy jsou data „natvrdo“ zadána v kódu, má určitý význam, ale praktické využití @dataProvider je samozřejmě v tom, že metoda bude připravovat data programově.

A tato programová příprava má dva typické způsoby. V tom prvním jsou testovací data vypočítána, často i s nějakým prvkem pseudonáhodnosti. A ve druhém způsobu jsou načítána ze souboru – viz další část.

Ukázka programové přípravy elementu jako pole dvou prvků typu int, které bude opět klíčované.

<?php
namespace app\parametrizovane;

use app\parametrizovane\NasobilkaPeti;
use PHPUnit\Framework\TestCase;

class DataProviderTest extends TestCase
{
  /**
   * @dataProvider pripravaDat
   */
  public function testMalaNasobilka(int $vstup, int $ocekavano) : void
  {
    $this->assertEquals($ocekavano, NasobilkaPeti::vypocti($vstup));
  }

  public function pripravaDat() : array
  {
    $pole = array();
    for ($i = 1;  $i <= 5;  $i++) {
      $element = array($i, $i * 5);
      $klic = $i . " * 5";
      $pole[$klic] = $element;
    }
    return $pole;
  }
}

Po spuštění se zobrazí:


Autor: Pavel Herout

11.5. N-tice parametrů načtená ze souboru – @dataProvider

Modifikací předchozího příkladu by bylo možné připravit seznam libovolných N-tic, kdy N-tice je typu array. Může se ale stát, že nám typ array z nějakého důvodu nevyhovuje, Případně se v naší testovací sadě vícekrát vyskytuje nějaká konkrétní struktura dat. Pak je možné uvažovat o použití návrhového vzoru Přepravka. Tento návrhový vzor má oproti předchozímu array tu výhodu, že jako objekt může poskytovat další služby, které do něj můžeme libovolně doprogramovat. V našem případě to bude metoda metoda __toString(), která bude využita pro správnou informaci o datasetu při výpisu parametrizovaného testu.

Poznámka: Příklad může vypadat zbytečně složitě, ale je to ukázka, jak vytvořit a předat libovolnou N-tici jako objekt. A objekt Přepravky samozřejmě nemusí vzniknout jen čtením ze souboru.

Bude načítán soubor seznam-predmetu.csv s následujícím obsahem:

Matika;1
Fyzika;2
Programování v Php;3

Třída přepravky PrepravkaPredmet doplněná o metodu __toString().
Poznámka: Kód Přepravky je značně zjednodušen – jsou použity public atributy místo getrů.

<?php
namespace app\parametrizovane;

class PrepravkaPredmet
{
  public $nazev;
  public $znamka;

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

  public function __toString() : string
  {
    return "[nazev='" . $this->nazev . "', znamka=" . $this->znamka . "]";
  }
}

Třída vlastního testu DataProviderSouborTest. Jak již bylo zmíněno, klíčem, tj. tím, co se vypisuje jako identifikace datasetu, je řetězec z __toString().

<?php
namespace app\parametrizovane;

use app\parametrizovane\PrepravkaPredmet;
use app\skutecnytest\HodnoceniPredmetu;
use PHPUnit\Framework\TestCase;

class DataProviderSouborTest extends TestCase
{
  /**
   * @dataProvider pripravaDat
   */
  public function testNastaveniPredmetu(PrepravkaPredmet $pp) : void
  {
    $predmet = new HodnoceniPredmetu($pp->nazev, $pp->znamka);
    $this->assertEquals($pp->nazev, $predmet->getNazev());
    $this->assertEquals($pp->znamka, $predmet->getZnamka());
  }

  public function pripravaDat() : array
  {
    $pole = array();
    $obsah = file_get_contents("..\..\..\seznam-predmetu.csv");
    $obsah = trim($obsah);
    $radky = explode("\n", $obsah);
    foreach ($radky as $radka) {
      $casti = explode(";", trim($radka));
      $predmet = new PrepravkaPredmet($casti[0], $casti[1]);
      $klic = $predmet->__toString();
      $pole[$klic] = array($predmet);
    }
    return $pole;
  }
}

Po spuštění se zobrazí:


Autor: Pavel Herout

Závěr

Předchozí minisérie o logování v PHP a tato o základech jednotkového testování pomocí PHPUnit byly připravovány s cílem poskytnout informace potřebné/důležité/zajímavé pro možné zvyšování kvality PHP aplikací.

Původní záměr byl dále pokračovat lehce pokročilejšími tématy z oblasti testování, jako je například mockování, problematika pokrytí kódu apod.

Root školení Elk

Protože však čtenost článků nebyla příliš vysoká, zdá se mi, že tato problematika z nějakých důvodů, které mi nepatří zde rozebírat, čtenáře příliš neoslovila. Z tohoto důvodu v historicky krátké době žádné pokračování neplánuji.

Pokud vás přesto články zaujaly, můj styl – například používání češtiny ;-)  – vám vyhovoval či minimálně nevadil a měli byste zájem o něco víc, můžete mne zkusit kontaktovat a domluvit se na nějaké formě další spolupráce.

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.