Kuba Werłos
to styl pisania przypadków testowych.
Przykład:
to narzędzie do pisania testów jednostkowych automatycznych.
Autorem jest Sebastian Bergmann i kontrybutorzy.
Pierwsza wersja została wydana w 2004 roku.
Według packagist.org został zainstalowany 117 milionów razy.
Jako zależność projektu:
composer require --dev phpunit/phpunit
./vendor/bin/phpunit --version
(lokalnie lub globalnie)
Ściągając plik PHAR:
wget https://phar.phpunit.de/phpunit-8.0.phar
php phpunit-8.0.phar --version
PHPUnit | PHP |
---|---|
^4.0 | 5.3+ |
^5.0 | 5.6+ |
^6.0 | 7.0+ |
^7.0 | 7.1+ |
^8.0 | 7.2+ |
phpunit.xml[.dist]
<?xml version='1.0' encoding='UTF-8'?>
<phpunit xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xsi:noNamespaceSchemaLocation='vendor/phpunit/phpunit/phpunit.xsd'
verbose='true'
>
<testsuites>
<testsuite name='all'>
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
<?xml version='1.0' encoding='UTF-8'?>
<phpunit backupGlobals='false'
bootstrap='./vendor/autoload.php'
cacheResult='true'
cacheResultFile='.phpunit.result.cache'
columns='80'
enforceTimeLimit='false'
executionOrder='default'
forceCoversAnnotation='false'
processIsolation='false'
stopOnFailure='false'
>
</phpunit>
phpunit -c tests/configuration-for-ci-server.xml
phpunit --testsuite Unit
phpunit tests/MyModule/MyFeatureTest.php
phpunit tests/MyFeatureTest.php --filter testSomething
Nazwa musi zaczynać się od „test” lub posiadać adnotację „@test”.
final class MyService
{
public function getName() { /* ... */ }
public function getCalculation() { /* ... */ }
}
final class MyServiceTest extends TestCase
{
public function testGetName() { /* ... */ }
public function testGetCalculation() { /* ... */ }
}
final class MyServiceTest extends TestCase
{
/**
* @test
*/
public function getName() { /* ... */ }
/**
* @test
*/
public function getCalculation() { /* ... */ }
}
final class MyConverterTest extends TestCase
{
public function testEmptyInput() { /* ... */ }
public function testNumberInput() { /* ... */ }
public function testSingleCharacterInput() { /* ... */ }
public function testLongStringInput() { /* ... */ }
}
final class MyConverterTest extends TestCase
{
/**
* @dataProvider provideConversionCases
*/
public function testConversion($expected, $input) { /* ... */ }
public function provideConversionCases()
{
return [
'empty input' => [ /* ... */ ],
'number input' => [ /* ... */ ],
'single character input' => [ /* ... */ ],
'long string input' => [ /* ... */ ],
];
}
}
PHPUnit posiada ponad 100 asercji, między innymi:
final class MyServiceTest extends TestCase
{
public function testSomething()
{
/* ... */
$this->assertTrue($value);
static::assertTrue($value);
Assert::assertTrue($value);
assertTrue($value);
}
}
final class MyServiceTest extends TestCase
{
public function testSomething1()
{
/* ... */
$this->assertSame($expected, $actual, 'Values are different');
}
public function testSomething2()
{
/* ... */
$this->assertJsonStringEqualsJsonFile($expected, $actual);
}
}
final class MyServiceTest extends TestCase
{
public function testInvalidType()
{
$service = new MyService();
$this->expectException(CustomException::class);
$this->expectExceptionCode(418);
$this->expectExceptionMessage('Not allowed type');
$service->convert(-10);
}
}
(ang. test doubles)
Używamy ich zamiast prawdziwego obiektu podczas testów.
Wyróżniamy 5 typów.
Ma zastosowanie gdy od obiektu nie oczekujemy niczego.
Zazwyczaj jest używany do uzupełnienia list argumentów.
$this->createMock(LoggerInterface::class);
Używany jest wtedy gdy potrzebujemy konkretnych wartości dla testu.
$animal = $this->createMock(Animal::class);
$animal
->method('numberOfLegs')
->willReturn(100);
Posiada działającą implementację, zwykle uproszczoną w celu uniknięcia efektów ubocznych (np. InMemoryTestDatabase).
$fruit = $this->createMock(FruitInterface::class);
$fruit->method('getColour')
->willReturnCallback(function ($fruit) {
$colours = ['apple' => 'red', 'banana' => 'yellow'];
if (isset($colours[$fruit])) {
return $colours[$fruit];
}
return 'unknown';
});
Używany gdy potrzebujemy zebrać informacje co się dzieje
z makietą podczas testu.
$sentMessages = [];
$logger = $this->createMock(MessageSender::class);
$logger->method('send')
->willReturnCallback(function ($message) use (&$sentMessages) {
$sentMessages[] = $message;
});
Oprócz tego co robią poprzednie makiety dodatkowo określa oczekiwania przed wykonaniem testu.
$animal = $this->createMock(Animal::class);
$animal
->expects($this->once())
->method('feed')
->with('Meat');
Makieta zwracająca makietę najprawdopodobniej wskazuje,
że zależności nie są prawidłowo zdefiniowane
(Prawo Demeter).
Słowo „mock” w kontekście PHPUnit oznacza makietę
i tylko od nas zależy jak jej użyjemy.
Poprawne rezultaty
$validator = new Validator();
assertTrue($validator->isValid('fineValue'));
assertFalse($validator->isValid('invalidValue'));
Niezmienniki – struktura
$shoppingCart = new ShoppingCart();
$shoppingCart->addProduct(new Product('book'), 1);
$shoppingCart->addProduct(new Product('game'), 1);
$productsList = $shoppingCart->getProductsList();
assertEquals(new Product('book'), productsList->getAtPosition(1));
assertEquals(new Product('game'), $productsList->getAtPosition(2));
Niezmienniki – logika
$shoppingCart = new ShoppingCart();
$shoppingCart->addProduct(new Product('book'), 2);
$shoppingCart->addProduct(new Product('book'), 4);
assertSame(6, $shoppingCart->getProductQuantity(new Product('book'));
Warunki brzegowe
assertFalse(DateService::isWorkingDay(false));
Warunki brzegowe – zgodność z oczekiwanym formatem
$nameForEmailExtractor = new NameForEmailExtractor();
expectException(InvalidEmailException::class);
$nameForEmailExtractor->extract('jan.kowalski!example.com');
Warunki brzegowe – czy wartość należy do określonego przedziału
expectException(InvalidLatitudeException::class);
$latitude = new Latitude(90.5);
<?xml version='1.0' encoding='UTF-8'?>
<phpunit>
<filter>
<whitelist>
<directory>./src</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name='coverage'>
<directory>./tests</directory>
<exclude>./tests/Functional</exclude>
</testsuite>
</testsuites>
<logging>
<log type='coverage-html' target='var/test-report' />
<log type='coverage-text' target='php://stdout' showOnlySummary='true' />
</logging>
</phpunit>
$ ./vendor/bin/phpunit
PHPUnit 8.0.5 by Sebastian Bergmann and contributors.
Runtime: PHP 7.3.3-1+ubuntu16.04.1+deb.sury.org+1
Time: 42.38 seconds, Memory: 206.50 MB
Tests: 13849, Assertions: 398464.
$ ./vendor/bin/phpunit
Runtime: PHP 7.3.3-1+ubuntu16.04.1+deb.sury.org+1 with Xdebug 2.7.0
Time: 39.61 minutes, Memory: 318.50 MB
Code Coverage: 79.61%
$ phpdbg -qrr ./vendor/bin/phpunit
Runtime: PHPDBG 7.3.3-1+ubuntu16.04.1+deb.sury.org+1
Time: 4.54 minutes, Memory: 432.50 MB
Code Coverage: 79.16%
$ ./vendor/bin/phpunit
Runtime: PHP 7.3.3-1+ubuntu16.04.1+deb.sury.org+1 with PCOV 1.0.0
Time: 1.46 minutes, Memory: 342.50 MB
Code Coverage: 89.73%
assertTrue(empty($value));
assertTrue(file_exists($file));
assertEmpty($value);
assertFileExists($file);
/**
* @covers \MyProject\MyService
*/
final class MyServiceTest extends TestCase
{
/* ... */
}
/**
* @coversNothing
*/
final class MyFunctionalTest extends TestCase
{
/* ... */
}
public function testSomethingWithCurrentTime()
{
/* poniższa wartość jest przypadkowa */
$timestamp = time();
}
public function testSomethingWithFixedDate() {
/* poniżej będzie zawsze ten sam dzień */
$date = date_create_from_format('j-M-Y', '26-Mar-2019');
};