I dobří programátoři dělají chyby. Rozdíl mezi dobrým a špatným programátorem je v tom, že ten dobrý ji rychle odhalí pomocí automatizovaných testů.

  • „Kdo netestuje, je odsouzen opakovat své chyby.“ (přísloví)
  • „Jakmile se zbavíme jedné chyby, objeví se další.“ (Murphyho zákon)
  • „Testovat, testovat, testovat“ (Martin Iljič Fowler)

Už jste někdy napsali v PHP podobný kód?

$obj = new MyClass;
$result = $obj->process($input);

var_dump($result);

Tedy vypsali jste si výsledek volání funkce jen proto, abyste okem ověřili, zda vrací to, co má? Určitě to děláte mnohokrát denně. V případě, že vše funguje správně, smažete tento kód a očekáváte, že se třída v budoucnu nerozbije? Murphyho zákony garantují opak :-)

V podstatě jsme napsali test a kdybychom ho nesmazali, mohli bychom ho spustit kdykoliv v budoucnu a ověřit, že vše stále funguje jak má. Časem takových testů vytvoříte velké množství, tudíž by se hodilo je spouštět automatizovaně. Proto by bylo vhodné test mírně upravit, aby nevyžadoval naši kontrolu, aby se zkrátka zkontroloval sám.

A s tím vám pomůže testovací framework Nette Tester.

Instalace a požadavky

Minimální verze PHP vyžadována Testerem je 5.6.0.

Preferovaný způsob instalace je pomocí Composer a všechny dále uvedené ukázky předpokládají tuto instalaci. Nic však nebrání použití Testeru bez něj a na odlišnosti budeme průběžně upozorňovat.

Instalace Composerem

Předpokládejme, že máte Composer funkční, aplikace se jmenuje demo a má následující strukturu:

demo/
├── src/           # kód aplikace, který chceme testovat
├── tests/         # testy, které vytváříme
├── vendor/
└── composer.json

Na příkazové řádce přejdeme do adresáře demo a přidáme závislost na Tester a to pouze pro vývoj.

cd demo
composer require --dev nette/tester

Ruční instalace

Tester stáhneme z GitHub a rozbalíme, anebo naklonujeme jeho git repozitář git clone https://github.com/nette/tester.git. Adresářová struktura naší demo aplikace vypadá následovně:

demo/
├── src/           # kód aplikace, který chceme testovat
├── tester/        # kód staženého Testeru
│   ├── src/
│   ├── tests/
│   ├── ...
│   └── readme.md
│
└── tests/         # testy, které vytváříme

Spuštění Testeru

Tester spouštíme z příkazové řádky. Zkusme to, bez parametrů pouze vypíše nápovědu.

cd demo
php vendor/nette/tester/src/tester.php  # při instalaci Composerem
php tester/src/tester.php               # při ruční instalaci

Při použití Composeru doporučujeme spouštění ještě snadnější:

cd demo
vendor/bin/tester      # UNIX
vendor\bin\tester.bat  # Windows

Pro zjednodušení budeme dále uvádět pouze příkaz tester. Ke spuštění je ale nutné použít jeden z výše popsaných způsobů s úplnou cestou.

Spouštíme testy

Naše aplikace zatím žádné testy nemá. Vytvoříme jednoduchou třídu, kterou budeme testovat, a umístíme ji do souboru src/Greeting.php

<?php

class Greeting
{
    public function say($name)
    {
        if (!$name) {
            throw new InvalidArgumentException('Invalid name');
        }
        return "Hello $name";
    }
}

Nyní napíšeme test a uložíme ho do souboru tests/greeting.phpt. Nenechte se odradit jeho délkou, později si ukážeme, jak vše zjednodušit.

<?php

use Tester\Assert;

# Načteme knihovny Testeru.
require __DIR__ . '/../vendor/autoload.php';       # při instalaci Composerem
require __DIR__ . '/../tester/src/bootstrap.php';  # při ruční instalaci

# Načteme testovanou třídu. V praxi se o to zřejmě postará Composer
# anebo váš autoloader.
require __DIR__ . '/../src/Greeting.php';


# Upraví chování PHP a zapne některé vlastnosti Testeru (popsáno dále)
Tester\Environment::setup();


$o = new Greeting;

Assert::same('Hello John', $o->say('John'));  # Očekáváme shodu

Assert::exception(function() use ($o) {       # Očekáváme výjimku
    $o->say('');
}, InvalidArgumentException::class, 'Invalid name');

Test máme napsaný, můžeme ho z příkazové řádky poprvé spustit:

cd tests
php greeting.phpt

Test jsme spustili jako běžný PHP skript. Ikdyž se to zdá jako maličkost, je v tom ukryt velký potenciál. Test můžeme krokovat v IDE anebo načíst přes webový prohlížeč.

První spuštění nám odhalí syntaktické chyby a pokud jsme nikde neudělali překlep, test skončí bez chyby. Zkusme v testu změnit tvrzení na Assert::same('Hi John', $o->say('John')); a sledujme, co se při spuštění stane.

Jak aplikace roste, počet testů roste s ní. Nebylo by praktické spouštět testy po jednom. Pro spuštění použijeme Tester:

cd demo
tester tests/greeting.phpt  # spustíme pouze jeden test
tester tests                # spustíme všechny testy v adresáři

A abychom viděli, jak vypadá spuštění více testů, zkusme spustit všechny testy, které testují samotný Tester. Jsou součástí jeho instalace:

tester vendor/nette/tester/tests  # při instalaci Composerem
tester tester/tests               # při ruční instalaci

Princip spuštění

Tester projde zadané adresáře, vytvoří si seznam nalezených testů a potom je provádí. Každý test spustí jako nový PHP proces, každý test tak probíhá zcela izolovaně od ostatních.

Tester vyhledává *.phpt a *Test.php soubory. Jako první spouští testy, které při předchozím běhu selhali.

Tester spouští PHP procesy s parametrem -n, tedy bez php.ini. Detailněji v kapitole Vlastní php.ini.

Vyhodnocení testů

Během provádění testů vypisuje Tester výsledky průběžně na terminál jako:

  • . (tečka) = test prošel
  • s = test byl přeskočen (skipped)
  • F = test selhal (failed)

Výstup může vypadat takto:

 _____ ___  ___ _____ ___  ___
|_   _/ __)( __/_   _/ __)| _ )
  |_| \___ /___) |_| \___ |_|_\  v2.0.0

PHP 7.1.3 (cli) | php -n | 8 threads

........s................F.........

-- FAILED: tests/greeting.phpt
   Failed: 'Hello John' should be
       ... 'Hi John'

   in src/Framework/Assert.php(370)
   in src/Framework/Assert.php(52) Tester\Assert::fail()
   in tests/greeting.phpt(6) Tester\Assert::same()


FAILURES! (35 tests, 1 failures, 1 skipped, 1.7 seconds)

Bylo spuštěno 35 testů, jeden selhal, jeden byl přeskočen. Pokud žádný test neselže, návratový kód Testeru je nula. Jinak je návratový kód nenulový.

Environment::setup()

V testech bylo zmíněno trochu tajemné Tester\Environment::setup() volání. Co to dělá?

  • zlepší čitelnost výpisu chyb (včetně obarvování), jinak je vypsán výchozí PHP stack trace
  • zapne kontrolu, že byly v testu volány aserce, jinak test bez asercí (například zapomenutých) projde také
  • při použití --coverage spustí automaticky sběr informací o spuštěném kódu (popsáno dále)

Použití je volitelné, ale doporučované.

Environment::RUNNER

Může být užitečné vědět, jestli jsme test spustili ručně, anebo pomocí Testeru.

if (getenv(Tester\Environment::RUNNER)) {
    # spuštěno Testerem
} else {
    # spuštěno jinak
}

Environment::THREAD

Dále se dozvíme, že Tester spouští testy paralelně v zadaném počtu vláken. Pokud nás zajímá číslo vlákna, zjistíme ho z proměnné prostředí:

echo "Běžím ve vlákně číslo " . getenv(Tester\Environment::THREAD).

Adresářová struktura

Může se zdát předčasné o tom hovořit, ale dobře strukturované adresáře s testy ušetří hodně práce. Testy rozdělíme do podadresářů podle jmenného prostoru testované třídy:

demo/
└── tests/
    ├── NamespaceOne/
    │   ├── MyClass.getUsers.phpt
    │   ├── MyClass.setUsers.phpt
    │   └── ...
    │
    ├── NamespaceTwo/
    │   ├── MyClass.creating.phpt
    │   ├── MyClass.dropping.phpt
    │   └── ...
    │
    ├── test.one.phpt
    ├── test.two.phpt
    ├── ...
    └── bootstrap.php

a vytvoříme si soubor bootstrap.php. Ten obsahuje kód společný pro všechny testy, například autoloading tříd, konfiguraci prostředí, vytvoření dočasného adresáře, pomocné funkce a podobně. Všechny testy bootstrap načítají a dále se věnují pouze testování. Bootstrap může vypadat následovně:

require __DIR__ . '/../vendor/autoload.php';

Tester\Environment::setup();
date_default_timezone_set('Europe/Prague');

define('TMP_DIR', '/tmp/demo-app-tests');

Se změnou adresářové struktury se spouštění testů nijak neliší. Tester najde rekurzivně všechny *.phpt a *Test.php testy a spustí je:

cd demo
tester tests

Můžeme ale snadno spustit testy pro jediný namespace:

tester tests/NamespaceOne

Aserce

Na začátku jsme v ukázkovém testu použili dvě aserce: Assert::same() a Assert::exception(). Nyní si ukážeme i všechny ostatní a vysvětlíme, jak se používají. Jsou to metody třídy Tester\Assert, ale pro zjednodušení je dále uvádíme bez jmenného prostoru.

Assert::same($expected, $actual)

$expected musí být totožný s $actual. To samé jako PHP operátor ===.

Assert::notSame($expected, $actual)

Opak Assert::same().

Assert::equal($expected, $actual)

$expected musí být stejný s $actual. Ignoruje se identita objektů a pořadí klíčů v polích.

Assert::notEqual($expected, $actual)

Opak Assert::equal().

Assert::contains($needle, $actual)

$actual musí obsahovat $needle. Pokud je $actual řetězec, musí obsahovat podřetězec $needle. Pokud je pole, musí obsahovat prvek $needle.

Assert::notContains($needle, $actual)

Opak Assert::contains().

Assert::true($value)

$value musí být true, tedy $value === true.

Assert::truthy($value)

$value musí být pravdivý, tedy $value == true.

Assert::false($value)

$value musí být false, tedy $value === false.

Assert::falsey($value)

$value musí být nepravdivý, tedy $value == false.

Assert::count($count, $value)

$count musí být počet prvků ve $value. Počitatelné je pole nebo objekt implementující Countable.

Assert::null($value)

$value musí být null, tedy $value === null.

Assert::nan($value)

$value musí být Not a Number.

Assert::type($type, $value)

$value musí být daného typu. Jako $type můžeme použít řetězec:

  • array
  • list – je jako array, ale indexy musí být od nuly a číslované po jedné
  • bool
  • callable
  • float
  • int nebo integer
  • null
  • object
  • resource
  • scalar
  • string
  • název třídy nebo přímo objekt, potom musí být $value instanceof $type

Assert::exception($callable, $class, $message = null, $code = null)

Při zavolání $callable musí být vyhozena výjimka instance $class. Pokud uvedeme $message, musí pasovat (viz. Assert::match()) i zpráva výjimky a pokud uvedeme $code, musí se shodovat i kódy. Následující test selže, protože neodpovídá zpráva výjimky:

Assert::exception(function () {
    throw new App\InvalidValueException('Zero value');
}, App\InvalidValueException::class, 'Value is to low');

Assert::exception() je zvláštní tím, že vrací vyhozenou výjimku. Lze tak otestovat i výjimku předchozí.

$e = Assert::exception(function () {
    throw new MyException('Something is wrong', 0, new RuntimeException);
}, MyException::class, 'Something is wrong');

Assert::type(RuntimeException::class, $e->getPrevious());

Assert::error($callable, $type, $message = null)

Pokud jako $type uvedete název třídy, chová se aserce naprosto stejně jako Assert::exception().

Pokud je $type jedna z konstant E_..., tedy například E_WARNING, musí $callable při zavolání tuto chybu vygenerovat. A pokud uvedeme $message, musí pasovat (viz. Assert::match()) i chybová zpráva. Například:

Assert::error(function () {
    $i++;
}, E_NOTICE, 'Undefined variable: i');

A poslední varianta, pokud je $type pole, musí $callable vygenerovat všechny takto očekávané chyby. Nejlépe asi vysvětlí příklad:

Assert::error(function () {
    $a++;
    $b++;
}, [
    [E_NOTICE, 'Undefined variable: a'],
    [E_NOTICE, 'Undefined variable: b'],
]);

Assert::noError($callable)

Kontroluje, že funkce $callable nevygenerovala žádné varování, chybu nebo výjimku. Hodí se pro testování kousků kódu, kde není žádná další aserce.

Assert::match($pattern, $actual)

$actual musí vyhovět vzoru $pattern. Můžeme použít dvě varianty vzorů.

Jako $pattern předáme regulární výraz. K jeho ohraničení musíme použít ~ nebo #, jiné oddělovače nejsou podporovány. Například test, kdy $var musí obsahovat pouze hexadecimální číslice:

Assert::match('#^[0-9a-f]$#i', $var);

Druhá varianta je podobná běžnému porovnání řetězců, ale v $pattern můžeme použít různé modifikátory:

  • %a% one or more of anything except the end of line characters
  • %a?% zero or more of anything except the end of line characters
  • %A% one or more of anything including the end of line characters
  • %A?% zero or more of anything including the end of line characters
  • %s% one or more white space characters except the end of line characters
  • %s?% zero or more white space characters except the end of line characters
  • %S% one or more of characters except the white space
  • %S?% zero or more of characters except the white space
  • %c% a single character of any sort (except the end of line)
  • %d% one or more digits
  • %d?% zero or more digits
  • %i% signed integer value
  • %f% floating point number
  • %h% one or more HEX digits
  • %w% one or more alphanumeric characters
  • %% one % character

Příklady:

# Opět test na hexadecimální číslo
Assert::match('%h%', $var);

# Zobecnění cesty k souboru a čísla řádky
Assert::match('Error in file %a% on line %i%', $errorMessage);

Assert::matchFile($file, $actual)

Aserce je totožná s Assert::match(), ale vzor se načítá ze souboru $file. To je užitečné pro testování velmi dlouhých řetězců. Soubor s testem zůstane přehledný.

Assert::fail($message, $actual = null, $expected = null)

Tato aserce vždy selže. Někdy se to prostě hodí. Volitelně můžeme uvést i očekávanou a aktuální hodnotu.

Zkoumání chybných asercí

Když aserce selže, Tester vypíše, v čem je chyba. Pokud porovnáváme složitější struktury, Tester vytvoří dumpy porovnávaných hodnot a uloží je do adresáře output. Například při selhání smyšleného testu Arrays.recursive.phpt budou dumpy uloženy následovně:

demo/
└── tests/
    ├── output/
    │   ├── Arrays.recursive.actual    # aktuální hodnota
    │   └── Arrays.recursive.expected  # očekávaná hodnota
    │
    └── Arrays.recursive.phpt          # selhávající test

Název adresáře můžeme změnit přes Tester\Dumper::$dumpDir.

Přeskakování testů

Některé testy mohou běžet pouze za určitých podmínek. Pokud nejsou tyto podmínky splněny, můžeme test přeskočit. Například testy jen pro určitý operační systém:

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
    Tester\Environment::skip('Requires UNIX.');
}

Test bude ukončen a označen jako přeskočený (s – skipped). Později se dozvíme, jak přeskočit test pomocí anotace @skip.

TestCase

V úvodu jsme si ukázali jednoduchý test, kdy aserce následovaly jedna za druhou. Někdy je výhodnější aserce zabalit do testovací třídy a tak je strukturovat. Třída musí být potomkem Tester\TestCase a zjednodušeně o ní mluvíme jako o testcase. Třída musí obsahovat testovací metody začínající na test. Tyto metody budou spuštěny jako testy:

use Tester\Assert;

class GreetingTest extends Tester\TestCase
{
    public function testOne()
    {
        Assert::same(......);
    }

    public function testTwo()
    {
        Assert::match(......);
    }
}

# Spuštění testovacích metod
(new GreetingTest)->run();

Takto napsaný test lze dále obohatit o metody setUp() a tearDown(). Jsou volány před, resp. za každou testovací metodou:

use Tester\Assert;

class NextTest extends Tester\TestCase
{
    public function setUp()
    {
        # Příprava
    }

    public function tearDown()
    {
        # Úklid
    }

    public function testOne()
    {
        Assert::same(......);
    }

    public function testTwo()
    {
        Assert::match(......);
    }
}

# Spuštění testovacích metod
(new NextTest)->run();

/*
Pořadí volání metod
-------------------
setUp()
testOne()
tearDown()

setUp()
testTwo()
tearDown()
*/

Pokud dojde k chybě v setUp() nebo tearDown() fázi, test celkově selže. Pokud dojde k chybě v testovací metodě, i přes to se metoda tearDown() spustí, avšak s potlačením chyb v ní.

Anotace pro TestCase metody

U testovacích metod máme k dispozici několik anotací, které nám testování usnadní. Zapisujeme je k testovací metodě.

@throws

Je ekvivalentem použití Assert::exception() uvnitř testovací metody. Zápis je ale přehlednější:

/**
 * @throws RuntimeException
 */
public function testOne()
{
    ...
}


/**
 * @throws LogicException  Wrong argument order
 */
public function testTwo()
{
    ...
}

@dataProvider

Chceme-li testovací metodu spustit vícekrát, ale s jinými parametry, hodí se právě tato anotace. Za ní uvedeme název metody, která vrací argumenty pro testovací metodu. Metoda musí vrátit pole nebo Traversable. Jednoduchý příklad:

public function getLoopArgs()
{
    return [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9],
    ];
}


/**
 * @dataProvider getLoopArgs
 */
public function testLoop($a, $b, $c)
{
    ...
}

Druhá varianta anotace @dataProvider přijímá jako parametr cestu k INI souboru (relativně k souboru s testem). Metoda je volána tolikrát, kolik je v INI souboru sekcí. Soubor loop-args.ini:

[one]
a=1
b=2
c=3

[two]
a=4
b=5
c=6

[three]
a=7
b=8
c=9

a metoda, která INI soubor využívá:

/**
 * @dataProvider loop-args.ini
 */
public function testLoop($a, $b, $c)
{
    ...
}

Obdobně můžeme namísto INI souboru odkázat na PHP skript. Ten musí vrátit pole nebo Traversable. Soubor loop-args.php:

return [
   ['a' => 1, 'b' => 2, 'c' => 3],
   ['a' => 4, 'b' => 5, 'c' => 6],
   ['a' => 7, 'b' => 8, 'c' => 9],
];

Anotování souboru s testem

Následující anotace jsou vyhodnoceny pouze pokud test spustíme pomocí Testeru, nikoliv ručně jako běžný PHP skript.

Anotace se zapisují na začátek souboru s testem a nebere se ohled na velikost písmen. Smyšlený příklad:

/**
 * TEST: Basic database query test.
 *
 * @dataProvider files/databases.ini
 * @exitCode 56
 * @phpVersion < 5.5
 */

require __DIR__ . '/../bootstrap.php';

TEST:

To vlastně ani není anotace, pouze určuje nadpis testu, který se vypisuje při selhání nebo do logu.

@skip

Test se přeskočí. Hodí se pro dočasné vyřazení testů.

@phpVersion

Test se přeskočí pokud není spuštěn s odpovídající verzí PHP. Anotaci zapisujeme jako @phpVersion [operator] verze. Operátor můžeme vynechat, výchozí je >=. Příklady:

/**
 * @phpVersion 5.3.3
 * @phpVersion < 5.5
 * @phpVersion != 5.4.5
 */

@phpExtension

Test se přeskočí, pokud nejsou načtena všechna uvedená PHP rozšíření. Více rozšížení můžeme uvést v jedné anotaci, nebo ji použít vícekrát.

/**
 * @phpExtension pdo, pdo_pgsql, pdo_mysql
 * @phpExtension json
 */

@dataProvider

Zapisujeme jako @dataProvider file.ini, cesta k souboru se bere relativně k souboru s testem. Test bude spuštěn tolikrát, kolik je sekcí v INI souboru. Předpokládejme INI soubor databases.ini:

[mysql]
dsn = "mysql:host=127.0.0.1"
user = root
password = ******

[postgresql]
dsn = "pgsql:host=127.0.0.1;dbname=test"
user = postgres
password = ******

[sqlite]
dsn = "sqlite::memory:"

a ve stejném adresáři test database.phpt :

/**
 * @dataProvider databases.ini
 */

$args = Tester\Environment::loadData();

Test bude spuštěn třikrát a $args bude obsahovat vždy hodnoty ze sekce mysql, postgresql nebo sqlite.

Existuje ještě varianta, kdy anotaci zapíšeme s otazníkem jako @dataProvider? file.ini. V tomto případě se test přeskočí, pokud INI soubor neexistuje.

Tím možnosti anotace nekončí. Za název INI souboru můžeme specifikovat podmínky, za kterých bude test pro danou sekci spuštěn. Rozšíříme INI soubor:

[mysql]
dsn = "mysql:host=127.0.0.1"
user = root
password = ******

[postgresql 8.4]
dsn = "pgsql:host=127.0.0.1;dbname=test"
user = postgres
password = ******

[postgresql 9.1]
dsn = "pgsql:host=127.0.0.1;dbname=test;port=5433"
user = postgres
password = ******

[sqlite]
dsn = "sqlite::memory:"

a použijeme anotaci s podmínkou:

/**
 * @dataProvider  databases.ini  postgresql, >=9.0
 */

Test bude spuštěn pouze jednou a to pro sekci postgresql 9.1. Ostatní sekce filtrem podmínky neprojdou.

Obdobně můžeme namísto INI souboru odkázat na PHP skript. Ten musí vrátit pole nebo Traversable. Soubor databases.php:

return [
    'postgresql 8.4' => [
        'dsn' => '...',
        'user' => '...',
    ],

    'postgresql 9.1' => [
        'dsn' => '...',
        'user' => '...',
    ],
];

@multiple

Zapisujeme jako @multiple N, kde N je celé číslo. Test bude spuštěn právě N-krát.

@testCase

Anotace nemá parametry. Použijeme ji, pokud testy píšeme jako Tester\TestCase třídy. V tom případě je test spuštěn tolikrát, kolik má testovacích metod. Každá testovací metoda se pak spustí v samostatném procesu. To může výrazně urychlit celý proces testování.

@exitCode

Zapisujeme jako @exitCode N, kde N je návratový kód spuštěného testu. Je-li v testu například voláno exit(10), anotaci zapíšeme jako @exitCode 10 a pokud test skončí s jiným kódem, je to považováno za selhání. Pokud anotaci neuvedeme, je ověřen návratový kód 0 (nula).

@httpCode

Anotace se uplatní pouze pokud je PHP binárka CGI. Jinak se ignoruje. Zapisujeme jako @httpCode NNN kde NNN je očekávaný HTTP kód. Pokud anotaci neuvedeme, ověřuje se HTTP kód 200. Pokud NNN zapíšeme jako řetězec vyhodnocený na nulu, například any, HTTP kód se neověřuje.

@outputMatch a @outputMatchFile

Funkce anotací je shodná s asercemi Assert::match() a Assert::matchFile(). Vzor (pattern) se ale hledá v textu, který test poslal na svůj standardní výstup. Uplatnění najde pokud předpokládáme, že test skončí fatal errorem a my potřebujeme ověřit jeho výstup.

@phpIni

Pro test nastavuje konfigurační INI hodnoty. Zapisujeme například jako @phpIni precision=20 a funguje stejně, jako kdybychom zadali hodnotu z příkazové řádky přes parametr -d precision=20.

Helpery

DomQuery

Tester\DomQuery je třída usnadňující testování obsahu HTML nebo XML. Popis všech metod najdeme v API dokumentaci, zde si ukážeme základní použití:

# HTML, které chceme testovat
$html = $template->render();

# Z HTML vytvoříme DOM strukturu
$dom = Tester\DomQuery::fromHtml($html);

# Můžeme testovat
Assert::true($dom->has('form#registration'));
Assert::true($dom->has('input[name="username"]'));
Assert::true($dom->has('input[name="password"]'));
Assert::true($dom->has('input[type="submit"]'));

Jako selektor metody DomQuery::has($selector) používáme CSS selektory.

FileMock

Tester\FileMock emuluje v paměti soubory a usnadňuje testování kódu, který používá funkce fopen(), file_get_contents(), parse_ini_file() a podobné. Příklad použití:

# Testovaná třída
class Logger
{
    private $logFile;

    public function __construct($logFile)
    {
        $this->logFile = $logFile;
    }

    public function log($message)
    {
        file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
    }
}

# Nový prázdný soubor
$file = Tester\FileMock::create('');

$logger = new Logger($file);
$logger->log('Login');
$logger->log('Logout');

# Testujeme vytvořený obsah
Assert::same("Login\nLogout\n", file_get_contents($file));

purge()

Třída Tester\Helpers nám nabízí metodu purge(), která vytváří zadaný adresář, ale pokud ten již existuje, smaže celý jeho obsah. Hodí se pro vytvoření dočasného adresáře. Například v tests/bootstrap.php:

@mkdir(__DIR__ . '/tmp');  # @ - adresář již může existovat

define('TEMP_DIR', __DIR__ . '/tmp/' . getmypid());
Tester\Helpers::purge(TEMP_DIR);

lock()

Dále se dozvíme, že se testy spouštějí paralelně. Někdy ovšem potřebujeme, aby se běh testů nepřekrýval. Typicky u databázových testů je nutné, aby si test připravil obsah databáze a jiný test mu po čas běhu do databáze nesahal. V těchto testech použijeme Tester\Environment::lock($name, $dir):

Tester\Environment::lock('database', __DIR__ . '/tmp');

První parametr je jméno zámku, druhý je cesta k adresáři pro uložení zámku. Test, který získá zámek první proběhne, ostatní testy musí počkat na jeho dokončení.

bypassFinals()

Třídy anebo metody označené jako final se obtížně testují. Volání Tester\Environment::bypassFinals() na začátku testu způsobí, že jsou klíčová slova final během načítání kódu vypuštěna.

require __DIR__ . '/bootstrap.php';

Tester\Environment::bypassFinals();

class MyClass extends NormallyFinalClass  # <-- NormallyFinalClass už není final
{
    ...
}

Parametry příkazové řádky

Přehled všech voleb příkazové řádky získáme spuštěním Testeru bez parametrů, anebo s parametrem -h.

-p <path>

Pro spouštění testů použije Tester tuto PHP binárku:

tester -p /home/user/php-7.2.0-beta/php-cgi tests

-c <path>

Volba je stejná jako pro PHP binárku. Pro testy bude použit zadaný php.ini. Ve výchozím stavu se žádný php.ini nepoužije. Příklad, kdy distribuujeme php.ini s testy:

tester -c tests/php.ini tests

-C

Použije se systémové php.ini. Na UNIXu také všechny příslušné INI soubory /etc/php/{sapi}/conf.d/*.ini.

-l | –log <path>

Do souboru bude zapsán průběh testování. Všechny selhané, přeskočené, ale i úspěšné testy:

tester --log /var/log/tests.log tests

-d <key=value>

Volba je stejná jako pro PHP binárku. Pro testy definuje INI hodnotu. Parametr může být použit vícekrát.

-s

Zobrazí se informace o přeskočených testech.

--stop-on-fail

Tester zastaví testování u prvního selhávajícího testu.

-j <num>

Určuje, kolik paralelních procesů s testy se spustí. Výchozí hodnota je 8. Chceme-li, aby všechny testy proběhly v sérii, použijeme hodnotu 1.

-o <console|tap|junit|none>

Nastaví formát výstupu. Výchozí je formát pro konzoli.

  • console: shodné s výchozím formátem, ale v tomto případě se nezobrazí ASCII logo
  • tap: TAP formát vhodný pro strojové zpracování
  • junit: XML formát JUnit, taktéž vhodný pro strojové zpracování
  • none: nic se nevypisuje

-w | –watch <path>

Po dokončení testování Tester neskončí, ale zůstane běžet a sleduje zadaný adresář. Při změně spustí testy znova. Parametr může být použit vícekrát. Hodí se při tvorbě a ladění testů.

-i | –info

Zobrazí informace o běhovém prostředí pro testy. Například:

tester -p /usr/bin/php7.1 -c tests/php.ini --info


PHP binary:
/usr/bin/php7.1

PHP version:
7.1.7-1+0~20170711133844.5+jessie~1.gbp5284f4 (cli)

Loaded php.ini files:
/var/www/dev/demo/tests/php.ini

PHP temporary directory:
/tmp

Loaded extensions:
Core, ctype, date, dom, ereg, fileinfo, filter, hash, ...

--setup <path>

Tester při startu načte zadaný PHP skript. V něm je k dispozici proměnná Tester\Runner\Runner $runner. Předpokládejme soubor tests/runner-setup.php s obsahem:

$runner->outputHandlers[] = new MyOutputHandler;

Tester spustíme:

tester --setup tests/runner-setup.php tests

--temp <path>

Nastaví cestu k adresáři pro dočasné soubory Testeru. Výchozí hodnota podle sys_get_temp_dir(). Není-li výchozí hodnota validní, budete upozorněni.

Pokud si nejsme jisti, jaký adresář se používá, spustíme Tester s parametrem --info.

--colors 1|0

Ve výchozím stavu Tester detekuje barevný terminál a obarvuje svůj výstup. Tato volba autodetekci přebíjí. Globálně můžeme obarvování nastavit systémovou proměnnou prostředí NETTE_TESTER_COLORS.

--coverage <path>

Tester vygeneruje report s přehledem, kolik zdrojového kódu testy pokrývají. Tato volba vyžaduje instalované PHP rozšíření Xdebug, anebo PHP 7 s PHPDBG SAPI, které je rychlejší. Přípona cílového souboru určuje jeho formát. Buď HTML anebo Clover XML.

tester tests --coverage coverage.html  # HTML report
tester tests --coverage coverage.xml   # Clover XML report

Při použití PHPDBG můžeme u obsáhlých testů narazit na selhání testu kvůli vyčerpání paměti. Sběr informací o pokrytém kódu je paměťově náročný. V tomto případě nám pomůže volání Tester\CodeCoverage\Collector::flush() uvnitř testu. Zapíše nasbíraná data na disk a paměť uvolní. Pokud sběr dat neprobíhá, nebo je použit Xdebug, volání nemá žádný efekt.

Ukázka HTML reportu s pokrytím kódu.

--coverage-src <path>

Použijeme současně s volbou --coverage. <path> je cesta ke zdrojovým kódům, pro které se report generuje.

Podpora HHVM

Tester do verze 1.7 podporoval HHVM 3.3.0 nebo vyšší. Podpora byla od verze Testeru 2 ukončena. Použití bylo snadné:

tester -p hhvm

Tipy

  • Není dobré používat exit() a die() k ukončení testu s chybovou zprávou. Například exit('Error in connection') ukončí test s návratovým kódem 0 (nula) což signalizuje úspěch. Vždy použijte echo 'Error in connection' a následně exit(1).
  • Vybírejte co nejvhodnější aserce. Například Assert::same($a, $b), nikoliv Assert::true($a === $b). Pouze v prvním případě dostaneme smysluplnou chybovou zprávu. V druhém případě pouze false should be true což nám o obsahu proměnných $a a $b nic neříká.
  • Pro testování NAN hodnoty (Not a Number) použijte Assert::nan(). Hodnota NAN je velmi specifická a aserce Assert::same() nebo Assert::equal() mohou fungovat neočekávaně.

Vlastní php.ini

Tester spouští PHP procesy s parametrem -n, žádné php.ini není načteno. V UNIXu ani ty z /etc/php/conf.d/*.ini. To zajistí co nejčistší prostředí pro běh testů, ale také vyřadí všechna PHP rozšíření běžně načtená systémovým PHP.

Chcete-li načítání systémových php.ini souborů zachovat, použijte parametr -C.

Pokud nějaká rozšíření nebo speciální INI nastavení pro testy potřebujete, doporučujeme vytvoření vlastního php.ini souboru, který bude distribuován s testy. Tester pak spouštíme s parametrem -c, například tester -c tests/php.ini tests, kde INI soubor může vypadat takto:

[PHP]

extension=php_pdo_mysql.dll
extension=php_pdo_pgsql.dll

memory_limit=512M

Spuštění Testeru v UNIXu se systémovým php.ini, například tester -c /etc/php/cli/php.ini nenačte ostatní INI z /etc/php/conf.d/*.ini. To je vlastnost PHP, ne Testeru.

Průběžné testování

Pokud zatím nepoužíváte průběžné testování, tento průvodce vám vše vysvětlí.