Testovanie v PHP pre začiatočníkov
01/2018 / Milan Herda / feudarium.com
Akým spôsobom testujete urobené zmeny?

Testujete celú aplikáciu alebo len zmenené časti?
Má manažment dôveru vo váš kód?

Sadol by si váš manažér do lietadla, ktorého systémy ste programovali vy?
Sadli by ste si vy do takého lietadla?

Automatizované testy
Napísaný test je jediný dôkaz, že ste si svoj kód vyskúšali

Napísaný test ochráni váš kód pred chybou v budúcnosti

Ako vyzerá typický test?
- pripraví si prostredie
- urobí nejakú akciu
- skontroluje výsledok akcie (urobí aserciu)
Čo by mal každý test spĺňať?
- je opakovateľný
- nezávislý na ostatných testoch
- neovplyvňuje iné testy
Druhy testov
* zjednodušene
Akceptačný test
- aplikácia sa testuje v celku
- navštívi stránku aplikácie
- urobí nejakú akciu
- a skontroluje, či stránka vyzerá tak, ako by mala
Druhy testov
Integračný test
- testuje spoluprácu viacerých jednotiek kódu
- typicky spolupráca viacerých tried
- spolupráca kódu a DB, prepojenia na API a pod.
Druhy testov
Unit test
- testuje jednu jednotku kódu (trieda, metóda)
- v absolútnej izolácii
Druhy testov
Príklady
Stiahnite si zdrojáky pre príklad:
goo.gl/gDNHa9
PHPUnit
Najrozšírenejší testovací framework pre PHP.
Dajú sa s ním robiť všetky druhy testov.
Testy sa píšu ako metódy triedy odvodenej od
PHPUnit\Framework\TestCase
PHPUnit - Inštalácia
composer require --dev phpunit/phpunit
PHPUnit - Spustenie
vendor/bin/phpunit
--bootstrap=vendor/autoload.php
--color
tests/
Akceptačný test
v PHPUnit
Testovací scenár:
Akceptačný test v PHPUnit
- otvoríme stránku
- nájdeme formulár
- vyplníme text nového todo
- odošleme
- skontrolujeme, že položka sa objavila na stránke
Pridanie novej položky do zoznamu
Čo potrebujeme?
Akceptačný test v PHPUnit
"Prehliadač" ovládateľný z PHP
composer require --dev fabpot/goutte
Napr. Goutte: postavený nad BrowserKit a DomCrawler zo Symfony
Napíšeme test
Akceptačný test v PHPUnit
namespace tests\Acceptance;
use Goutte\Client;
use PHPUnit\Framework\TestCase;
class AddTodoTest extends TestCase
{
public function testAddTodo()
{
$myTodoText = 'Môj prvý akceptačný test';
$client = new Client();
$crawler = $client->request('GET', 'http://localhost:8080/');
$form = $crawler->selectButton('Pridaj úlohu')->form();
$form->setValues([
'text' => $myTodoText,
]);
$crawler = $client->submit($form);
$this->assertEquals(200, $client->getResponse()->getStatus());
$nodes = $crawler->filter('li.list-group-item:contains("' . $myTodoText . '")');
$this->assertEquals(1, $nodes->count());
}
}
Spustíme test
Akceptačný test v PHPUnit
vendor/bin/phpunit --bootstrap=vendor/autoload.php --color tests/

Výsledok:
Tento test nespĺňa požiadavky na dobrý test
- nie je opakovateľný (už druhé spustenie vyvolá chybu)
- keďže zapisuje do spoločnej databázy môže ovplyvniť iné testy
Akceptačný test v PHPUnit
Oprava: pred spustením si pripravíme prostredie do vhodného stavu
Akceptačný test v PHPUnit
Testovacia fixtúra
Akceptačný test v PHPUnit
Fixný a známy stav objektov a dát, nad ktorým vieme spustiť test.
Pripravíme si databázovú fixtúru
Akceptačný test v PHPUnit
[]
tests/Acceptance/fixtures/addTodo.json
Pri spustení testu ju načítame
Akceptačný test v PHPUnit
// tests/Acceptance/AddTodoTest.php
protected function loadDbFixtures(string $fixturePath)
{
copy($fixturePath, __DIR__ . '/../../db/data.json');
}
public function testAddTodo()
{
$this->loadDbFixtures(__DIR__ . '/fixtures/addTodo.json');
$myTodoText = 'Môj prvý akceptačný test';
// ...
}
Zabezpečíme, že testy nebudú bežať nad vývojou databázou
Akceptačný test v PHPUnit
#!/bin/bash
cp db/data.json db/data.bak.json
vendor/bin/phpunit --bootstrap=vendor/autoload.php --color $@
mv db/data.bak.json db/data.json
Vytvoríme spustiteľný súbor bin/run-tests
Vyskúšame spustiť test
Akceptačný test v PHPUnit
bin/run-tests tests/

Výsledok:

Ako na fixtúry pre relačnú databázu?
Akceptačný/Integračný test v PHPUnit
- Oddelená db pre testy
- Transakcie: begin na začiatku a rollback na konci testu
- Bez transakcií: resetovanie obsahu stavu db
Integračný test v PHPUnit
Integračný test v PHPUnit
Testovací scenár:
- vytvoríme inštanciu TodoDataProvider
- zavoláme metódu findAll
- assertami overíme, že sa nám vrátili očakávané dáta
Načítanie všetkých todo z databázy
Integračný test v PHPUnit
Db fixtúra
[
{
"id": 11,
"text": "prve todo",
"createdAt": "2018-01-01 01:01:01",
"isDone": false
},
{
"id": 22,
"text": "druhe todo",
"createdAt": "2018-02-02 02:02:02",
"isDone": true
}
]
Súbor tests/Integration/DataProvider/fixtures/findAll.json
Integračný test v PHPUnit
Test
namespace tests\Integration\DataProvider;
class TodoDataProviderTest extends TestCase
{
public function testFindAll()
{
$pathToFile = __DIR__ . '/fixtures/findAll.json';
$converter = new TodoItemConverter();
$provider = new TodoDataProvider($pathToFile, $converter);
$items = $provider->findAll();
$this->assertCount(2, $items);
$firstTodo = $items[0];
$this->assertInstanceOf(TodoItem::class, $firstTodo);
$this->assertEquals(11, $firstTodo->getId());
$this->assertEquals('prve todo', $firstTodo->getText());
$this->assertEquals(
'2018-01-01 01:01:01',
$firstTodo->getCreatedAt()->format('Y-m-d H:i:s')
);
$this->assertEquals(false, $firstTodo->isDone());
}
}
Integračný test v PHPUnit
Napíšte test, ktorý otestuje fungovanie findAll nad prázdnou db
Testami by sme mali pokryť pokiaľ možno všetky situácie, ktoré vieme predvídať.
Unit test v PHPUnit
Testuje sa iba jedna jednotka kódu
Unit test v PHPUnit
Všetko, čo nepatrí tejto jednotke, musí byť nahradené dablérmi plne pod kontrolou testu.
Testovací scenár
Unit test v PHPUnit
- vytvoríme inštanciu TodoItemConverter
- zavoláme metódu convertToEntity
- assertami overíme, že sa nám vrátili očakávané dáta
Konverzia dát na entity
Test
namespace tests\Unit\DataConverter;
class TodoItemConverterTest extends TestCase
{
public function testConvertToEntity()
{
$converter = new TodoItemConverter();
$data = [
'id' => 111,
'text' => 'lorem ipsum',
'createdAt' => '2018-01-01 01:01:01',
'isDone' => false,
];
$entity = $converter->convertToEntity($data);
$this->assertInstanceOf(TodoItem::class, $entity);
$this->assertEquals($data['id'], $entity->getId());
$this->assertEquals($data['text'], $entity->getText());
$this->assertEquals($data['createdAt'], $entity->getCreatedAt()->format('Y-m-d H:i:s'));
$this->assertEquals($data['isDone'], $entity->isDone());
}
}
Unit test v PHPUnit
Napíšte test pre convertToArray
namespace tests\Unit\DataConverter;
class TodoItemConverterTest extends TestCase
{
public function testConvertToArray()
{
$converter = new TodoItemConverter();
$todo = new TodoItem();
$todo->setId(222)
->setText('Star Trek Ipsum')
->setCreatedAt(new \DateTime('2018-02-02 02:02:02'))
->setAsNotDone();
$data = $converter->convertToArray($todo);
$this->assertEquals(222, $data['id']);
$this->assertEquals('Star Trek Ipsum', $data['text']);
$this->assertEquals('2018-02-02 02:02:02', $data['createdAt']);
$this->assertFalse($data['isDone']);
}
}
Unit test v PHPUnit

Ide o unit test?
testConvertToArray pracuje s dvoma triedami:
- TodoItemConverter
- TodoItem
TodoItem je však entita (DTO, Value Object), takže to až tak nevadí
Striktne vzaté sa jedná o integračný test.
Unit test v PHPUnit
Testovací scenár
Unit test v PHPUnit
- vytvoríme inštanciu TodoRepository
- zavoláme metódu findAll
- assertami overíme, že sa nám vrátili očakávané dáta
Získavanie entít z repozitáru
Závislosť na TodoDataProvider nahradíme dablérom
Unit test v PHPUnit
Test
namespace tests\Unit\Repository;
class TodoRepositoryTest extends TestCase
{
public function testFindAll()
{
$dataProvider = /* Ako vytvoriť dataProvider? */;
$repository = new TodoRepository($dataProvider);
$items = $repository->findAll();
$expectedItems = /* ... */;
$this->assertEquals($expectedItems, $items);
}
}
Unit test v PHPUnit
Test
// ...
$dataProvider = /* Ako vytvoriť dataProvider? */;
// ...
Nesmie to byť inštancia triedy TodoDataProvider, pretože táto trieda nie je predmetom testu a nemáme ju v teste pod kontrolou.
Unit test v PHPUnit
Riešenie
class TestFindAllDataProvider extends TodoDataProvider
{
public function findAll()
{
return 'hodnota len pre účel testu';
}
}
Vytvoríme triedu, ktorá bude potomkom pôvodného provideru
a bude sa používať len pre tento test.

Unit test v PHPUnit
Mock
Takejto "náhradnej" triede sa hovorí "mock".
Našťastie ich nemusíme vytvárať ručne, ale existujú k tomu knižnice.
Môžeme použiť priamo PHPUnit.
Ale prečo by sme tancovali vo zvieracej kazajke, keď máme alternatívu?
Unit test v PHPUnit
Nainštalujeme Mockery
composer require --dev mockery/mockery
Unit test v PHPUnit
Dokončíme test
class TodoRepositoryTest extends TestCase
{
protected function tearDown()
{
Mockery::close();
}
public function testFindAll()
{
$itemA = Mockery::mock(TodoItem::class);
$itemB = Mockery::mock(TodoItem::class);
$dataProvider = Mockery::mock(TodoDataProvider::class);
$dataProvider->shouldReceive('findAll')->once()->andReturn([$itemA, $itemB]);
$repository = new TodoRepository($dataProvider);
$items = $repository->findAll();
$this->assertEquals([$itemA, $itemB], $items);
}
}
Unit test v PHPUnit
Test pre pridanie todo
public function testAddTodo()
{
$newId = 23;
$newItem = Mockery::mock(TodoItem::class);
$newItem->shouldReceive('getId')->andReturn($newId);
$itemA = Mockery::mock(TodoItem::class);
$itemA->shouldReceive('getId')->andReturn(11);
$itemB = Mockery::mock(TodoItem::class);
$itemB->shouldReceive('getId')->andReturn(22);
$dataProvider = Mockery::mock(TodoDataProvider::class);
$dataProvider->shouldReceive('findAll')->once()->andReturn([$itemA, $itemB]);
$newItem->shouldReceive('setId')->once()->with($newId)->andReturnSelf();
$dataProvider->shouldReceive('saveItems')->once()->with([
$itemA,
$itemB,
$newItem
]);
$repository = new TodoRepository($dataProvider);
$id = $repository->addTodo($newItem);
$this->assertEquals($newId, $id);
}
Unit test v PHPUnit
Volania statických metód?
Tieto sa najlepšie mockujú pomocou knižnice Patchwork
Záver
Kedy spúšťať testy?
Pri vývoji spúšťame priebežne testy aspoň pre nový kód.
Po dokončení tasku spustíme všetky testy.
Najlepšie je mať testy spúšťané aj v CI pre každý Pull Request a po každom merge-i.
Aké druhy testov by sme mali robiť?
Všetky!


Nespomaľuje písanie testov čas vývoja?
Keď sa s testami začína a všetci sa ich učia, tak áno
Ale
- funkctionalitu treba aj tak otestovať
- počas vývoja sa testuje niekoľkokrát to isté - prečo si to neautomatizovať?
- ako dlho trvá fixovanie bugov, ktoré sa našli v produkcii?
- koľko peňazí stojí chyba nájdená v produkcii?
Písanie testov vedie k tvorbe kvalitnejšieho kódu
Existujúce testy znižujú množstvo chýb v kóde
Vývoj ide rýchlejšie, keď je existujúci kód kvalitnejší a bez chýb

Ďakujem za pozornosť

Otázky?
Testovanie v PHP pre začiatočníkov
By Milan Herda
Testovanie v PHP pre začiatočníkov
Zjednodušený úvod do písania automatizovaných testov v PHP pre programátorov, ktorí zatiaľ testy nepísali.
- 469