10. Příjemné drobnosti
10.1. Podmínečné spuštění testů nebo jejich ignorování
Při skutečném testování se dříve nebo později dostaneme do situace, kdy se v existující sadě testů nachází test, který nechceme dočasně spustit. Pak není vhodné zakomentovat jeho tělo, a to z jednoduchého důvodu – pravděpodobně by se zapomnělo jej později odkomentovat.
Příklady známých či typických situací, kdy vyvstane tato potřeba:
- Píšeme testy pro dosud neúplný kód.
- Nejsou splněny nějaké podmínky pro běh testu – např. chybějící soubor s testovacími daty, neprovedené spojení do DB, atp. Pozor: nesplnění těchto podmínek ale neznamená, že je testovaný kód chybný
- Uprostřed testu zjistíme v nějaké podmínce, že nemá cenu pokračovat. Tento případ by se neměl stávat často, protože tělo testu má být co nejjednodušší.
Ve všech zmíněných případech je vhodné na dočasné nekonzistence v testech upozornit, ale tak, že „problémový“ test neselže. To řešíme tak, že v těle testu použijeme metodu markTestSkipped()
nebo markTestSkipped("zpráva, proč je zakázán")
. Takto doplněný test pak bude uveden v seznamu existujících testů s tím, že není prováděn / dokončen a bude vypsána příslušná zpráva.
<?php namespace app\drobnosti; use PHPUnit\Framework\TestCase; class SkippedTest extends TestCase { public function test_1() : void { $this->markTestSkipped(); $this->assertTrue(false); // úmyslné selhání } public function test_2() : void { $this->markTestSkipped("test se neprovádí"); $this->assertTrue(true); } public function test_3() : void { $this->assertTrue(true); } }
Po spuštění sady testů jsou ignorované (neprováděné) testy označeny šedou ikonou, takže se zobrazí:
Poznámka: Pokud by se metodamarkTestSkipped()
použila v metodě setUp()
, nespustil by se žádný test z dané třídy. To může být výhodné v případě, kdy by nebyla splněna nějaká podmínka pro provádění všech testů, např. by selhalo spojení do DB.
Poznámka: Existuje podobná metoda markTestIncomplete()
, která by se použila v případě, že by se jednalo o nedodělaný / neodladěný test.
10.2. Tagy (štítky) testů
Anotace @group
umožňuje přidělovat testům tagy a tím s nimi v budoucnu hromadně manipulovat. Platí, že pro jeden test lze použít i více tagů.
Pro jméno tagu platí trochu mírnější pravidla než pro identifikátory, tj. je možné používat rozšířenější skupinu znaků. Ovšem doporučení pro jména tagů je jednoznačné – pojmenovat je významově a relativně stručně (např. SMOKE
).
Anotaci @group
je možné použít na dvou místech. Tím prvním je označení jednotlivé testovací metody, přičemž platí, že jednu metodu můžeme označit i několikrát, samozřejmě vždy jiným identifikátorem. Druhou možností je označit celou třídu testů – pak bude tag platit pro všechny v ní uvedené testovací metody.
Základní důvod použití tagů je, že otagované testy pak lze výběrově spouštět. Pro to používáme přepínače:
--group tag
, pro test s tímto tagem, který má být spouštěn--exclude-group tag
, pro test s tímto tagem, který nemá být spouštěn
Příklad použití tagů. Na jménu testovacích metod v souvislosti se jmény tagů nezáleží – zde jsou použity pro názornost ve výpisu spuštění testů.
<?php namespace app\drobnosti; use PHPUnit\Framework\TestCase; class GroupTest extends TestCase { /** * @group Smoke */ public function test_Smoke() : void { $this->assertTrue(true); } /** * @group Smoke * @group Validator */ public function test_Smoke_Validator() : void { $this->assertTrue(true); } /** * @group Databaze */ public function test_Databaze() : void { $this->assertTrue(true); } }
Po spuštění skupiny testů běžným dosud používaným způsobem se zobrazí, že byly spuštěny všechny tři testy:
Pokud ale modifikujeme konfiguraci spouštění této testovací třídy:
a nastavíme v ní --group Smoke
pak se po spuštění testů se zobrazí, že byly spuštěny jen testy s tagem Smoke
A naopak, po nastavení v konfiguraci --exclude-group Smoke
budou spuštěny jen testy bez taguSmoke
(zde pouze jeden test)
10.3. Vnější závislost testů na operačním systému nebo na verzi Php
PHPUnit má propracované podmíněné vykonávání testů, což je realizováno pomocí anotace @requires
a jejích parametrů. Anotace se uvádí v dokumentačních závorkách a může být použita jak pro třídu testů, tak i pro jednotlivé testy.
/** * @requires PHP >= 5.3 */
Z množství existujících možností (podrobně viz v dokumentaci) jsou nejzajímavější:
- závislost na verzi Php – možnosti:
>
,=
,<
atp. - závislost na verzi PHPUnit – možnosti jako u závislosti na verzi Php
- závislost na operačním systému, která je navázána na Php konstantu
PHP_OS_FAMILY
– možnosti:Windows
,Linux
,MAC
Příklad závislostí, kde opět nezáleží na jménech testovacích metod.
<?php namespace app\drobnosti; use PHPUnit\Framework\TestCase; class VnejsiZavislostiTest extends TestCase { /** * @requires OSFAMILY Linux */ public function test_naLinux() : void { fwrite(STDOUT, "test běží: " . __METHOD__); $this->assertTrue(true); } /** * @requires OSFAMILY Windows */ public function test_naWindows() : void { fwrite(STDOUT, "test běží: " . __METHOD__); $this->assertTrue(true); } /** * @requires PHP >= 7.0 */ public function test_nePhp5() : void { fwrite(STDOUT, "test běží: " . __METHOD__); $this->assertTrue(true); } /** * @requires PHPUnit > 9 */ public function test_nePHPUnit8() : void { fwrite(STDOUT, "test běží: " . __METHOD__); $this->assertTrue(true); } }
Po spuštění sady testů se vypíše do konzole:
Operating system Linux is required. test běží: app\drobnosti\VnejsiZavislostiTest::test_naWindows test běží: app\drobnosti\VnejsiZavislostiTest::test_nePhp5 test běží: app\drobnosti\VnejsiZavislostiTest::test_nePHPUnit8 OK, but incomplete, skipped, or risky tests! Tests: 4, Assertions: 3, Skipped: 1.
a zobrazí se:
10.4. Vnitřní závislost testů na sobě
U dobře napsané sady jednotkových testů by nemělo záležet na pořadí jejich spouštění a testy by neměly na sobě záviset. Někdy (velmi zřídka) je však výhodné závislost použít, např. testuje se v tomto pořadí řetězec, který může být:
$s = null
$s = ""
(tj. „empty“)$s = "ahoj"
Pokud potřebujeme spouštět v definovaném pořadí testy, které na sobě závisejí, použijeme anotaci @depends
. Ta se opět píše do dokumentačního komentáře a zajistí, že pokud některý z testů selže, budou všechny na něm závislé testy následně ignorovány.
Příklad závislostí testů. V příkladu selže druhý test v pořadí testEmpty()
, tzn. že třetí test testAhoj()
bude ignorován. Z použití @depends
vidíme, že jejím parametrem je jméno testu, na kterém daný test závisí, tj. na jménech testů zde záleží.
<?php namespace app\drobnosti; use PHPUnit\Framework\TestCase; class VnitrniZavislostiTest extends TestCase { private $s = "ahoj"; public function testNotNull() : void { $this->assertNotNull($this->s); } /** * @depends testNotNull */ public function testEmpty() : void { $this->assertEmpty($this->s); } /** * @depends testEmpty */ public function testAhoj() : void { $this->assertEquals("ahoj", $this->s); } }
Po spuštění sady testů se vypíše do konzole:
Failed asserting that a string is empty. This test depends on "app\drobnosti\VnitrniZavislostiTest::testEmpty" to pass. FAILURES! Tests: 3, Assertions: 2, Failures: 1, Skipped: 1.
a zobrazí se:
Poznámka: PHPUnit umožňuje ještě větší provázanost závislostí, kdy test může pro závislý test připravovat data (viz příklad v manuálu). Bez skutečně vážného důvodu se toto v jednotkových testech nedoporučuje dělat. Využití by bylo v testech funkcionálních.
10.5. Anotace
V předchozích částech již byly ukázány anotace @group
, @requires
a @depends
. PHPUnit umožňuje použít dalších asi 25 anotací. Všechny mají společné to, že musejí být uvedeny v dokumentačním komentáři:
/** * @anotace */
10.5.1. Anotace nahrazující již známé postupy
Některé aktivity, které se provádějí již známým (tj. dříve popsaným) způsobem, mohou být zajištěny volitelně i pomocí anotací. Je otázkou, zda tento způsob zvolit, protože tím pravděpodobně znepřehledníme strukturu testů. V žádném případě není vhodné tyto možnosti navzájem míchat!
@test
– nahrazuje počáteční texttest
ve jménu testovací metody/** * @test */ public function notNull() : void { $this->assertNotNull($this->s); }
je stejné, jako původní
public function testNotNull() : void { $this->assertNotNull($this->s); }
- anotace fixtures
@before
– nahrazuje pojmenování metodysetUp()
/** * @before */ public function nejakeJmeno() : void { $this->predmet = new HodnoceniPredmetu("Matika"); }
je stejné, jako původní
public function setUp() : void { $this->predmet = new HodnoceniPredmetu("Matika"); }
@after
– nahrazuje pojmenování metodytearDown()
@beforeClass
– nahrazuje pojmenování metodysetUpBeforeClass()
@afterClass
– nahrazuje pojmenování metodytearDownAfterClass()
@author
– funguje jako anotace@group
je to čitelnější dělení podle jména tvůrce testu, ovšem otázka je, zda případné spouštění testů podle jejich tvůrců má praktický smysl@ticket
– funguje jako anotace@group
je to čitelnější dělení podle názvu (často čísla) tiketu. Toto označení rozhodně smysl má, protože se s výhodou použije při retestování (konfirmační testování).
10.5.2. Anotace pracující s pokrytím kódu
Problematika měření pokrytí kódu bude detailně vykládána v samostatném článku Strukturální testy. Zde budou detailně vysvětleny anotace @covers
, @codeCoverageIgnore
, @codeCoverageIgnoreStart
, @codeCoverageIgnoreEnd
, @coversDefaultClass
, @coversNothing
a @uses
.
10.5.3. Anotace úplnosti testu
PHPUnit hlídá, zda je v testu použita alespoň jedna aserce. Pokud ne, považuje test za „riskantní“ v kategorii Useless Tests.
Reakce na takovýto spuštěný test je dvojí. Za prvé o tom vypisuje chybové hlášení:
This test did not perform any assertions
A za druhé takovýto test považuje za selhaný.
Pokud test skutečně a smysluplně nemá používat žádnou aserci (ani např. test výjimky expectException()
), dá se tomuto hlášení a selhání testu zabránit dvěma způsoby:
- anotací
@doesNotPerformAssertions
, která se uvede před konkrétním testovacím případem - nastavením
--dont-report-useless-tests
v konfiguraci
Ukázka testů bez asercí – jejich jedinou činností je, že vždy jen vypíší do konzole jméno své metody.
<?php namespace app\anotace; use PHPUnit\Framework\TestCase; class NotAssertionTest extends TestCase { public function test_bezAserce() : void { fwrite(STDOUT, __METHOD__); } /** * @doesNotPerformAssertions */ public function test_bezAserce_disabled() : void { fwrite(STDOUT, __METHOD__); } }
Po běžném spuštění se vypíše:
app\anotace\NotAssertionTest::test_bezAserce This test did not perform any assertions app\anotace\NotAssertionTest::test_bezAserce_disabled OK, but incomplete, skipped, or risky tests! Tests: 2, Assertions: 0, Risky: 1.
a zobrazí se:
Pokud se změní konfigurace spouštění na:
pak se po spuštění vypíše:
app\anotace\NotAssertionTest::test_bezAserce app\anotace\NotAssertionTest::test_bezAserce_disabled OK (2 tests, 0 assertions)
a zobrazí se:
10.5.4. Anotace pro parametrizované testy
Tato problematika bude detailně vykládána ve článku Parametrizované testy. Zde budou detailně vysvětleny anotace @dataProvider
a @testWith
.