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 7.1. Detailněji v tabulce podporované verze PHP.

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
vendor/bin/tester            # při instalaci Composerem pod UNIX
vendor\bin\tester            # při instalaci Composerem pod Windows
php tester/src/tester.php    # při ruční instalaci

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.

Píšeme 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. I když 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 selhaly.

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

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é.

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

Podporované verze PHP

Tester \ PHP 5.3 5.4 5.5 5.6 7.0 7.1 7.2 7.3
0.9.0 – 1.7.1 Ano Ano Ano Ano Ano Ano ne ne
1.7.2 Ano Ano Ano Ano Ano Ano Ano Ano
2.0.* ne ne ne Ano Ano Ano Ano Ano
2.1.* ne ne ne ne ne Ano Ano Ano
dev (master) ne ne ne ne ne Ano Ano Ano

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

tester -p hhvm

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