Jednotkové testování v PHP: příjemné drobnosti

18. 11. 2021
Doba čtení: 8 minut

Sdílet

 Autor: Depositphotos
Kromě základních možností pro psaní testů nám PHPUnit poskytuje i řadu užitečných funkcí, které oceníme, jakmile začneme psát rozsáhlejší testovací sady. Hlavní z nich jsou vysvětleny na příkladech v tomto článku.

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


Autor: Pavel Herout

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:


Autor: Pavel Herout

Pokud ale modifikujeme konfiguraci spouštění této testovací třídy:


Autor: Pavel Herout

a nastavíme v ní --group Smoke


Autor: Pavel Herout

pak se po spuštění testů se zobrazí, že byly spuštěny jen testy s tagem Smoke


Autor: Pavel Herout

A naopak, po nastavení v konfiguraci --exclude-group Smoke


Autor: Pavel Herout

budou spuštěny jen testy bez taguSmoke (zde pouze jeden test)


Autor: Pavel Herout

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:


Autor: Pavel Herout

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:

  1. $s = null
  2. $s = "" (tj. „empty“)
  3. $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:


Autor: Pavel Herout

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í text test 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í metody setUp()
                /**
                 * @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í metody tearDown()
    • @beforeClass  – nahrazuje pojmenování metody setUpBeforeClass()
    • @afterClass  – nahrazuje pojmenování metody tearDownAfterClass()
  • @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:


Autor: Pavel Herout

Pokud se změní konfigurace spouštění na:


Autor: Pavel Herout

pak se po spuštění vypíše:

bitcoin_skoleni

    app\anotace\NotAssertionTest::test_bezAserce
    app\anotace\NotAssertionTest::test_bezAserce_disabled

    OK (2 tests, 0 assertions)

a zobrazí se:


Autor: Pavel Herout

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.

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.