Модульне тестування
Дата оновлення перекладу 2023-09-01
Модульне тестування
Ви могли помітити деякі маленькі, але тим не менш важливі, баги у фреймворку, який ми побудували у попередній главі. При створенні фреймворку, ви повинні бути впевнені, що він поводить себе так, як заявлено. Якщо ж ні, то всі додатки, засновані на ньому, матимуть однакові баги. Гарна новина в тому, що коли ви виправляєте один баг, ви виправляєте купу інших додатків.
Сьогоднішня місія - написати модульні тести для фреймворку, який ми створили, використовуючи PHPUnit. Спочатку, встановіть PHPUnit в якості залежності розробки:
1
$ composer require --dev phpunit/phpunit
Потім, Створіть файл конфігурації PHPUnit в example.com/phpunit.xml.dist
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>
Ця конфігурація визначає розумні значення за замовчуванням для більшості налаштувань
PHPUnit; більш того, автозавантажувач використовується для початкового завантаження
тестів, і тести зберігатимуться в каталозі example.com/tests/
.
Тепер давайте напишемо тест для "не знайдених" джерел. Щоб уникнути створення всіх залежностей при написанні тестів, і для того, щоб дійсно модульно протестувати те, що ми хочемо, ми використовуватимемо тестові дублі. Тестові дублі легше створити, коли ми покладаємося на інтерфейси, а не на конкретні класи. На щастя, Symfony надає такі інтерфейси для базових обʼєктів на кшталт зіставника URL та розвʼязувача контролерів. Змініть фреймворк так, щоб використовувати їх:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// example.com/src/Simplex/Framework.php
namespace Simplex;
// ...
use Calendar\Controller\LeapYearController;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
class Framework
{
public function __construct(
private UrlMatcherInterface $matcher,
private ControllerResolverInterface $resolver,
private ArgumentResolverInterface $argumentResolver,
) {
}
// ...
}
Тепер ми готові написати наш перший тест:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
// example.com/tests/Simplex/Tests/FrameworkTest.php
namespace Simplex\Tests;
use PHPUnit\Framework\TestCase;
use Simplex\Framework;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\Routing;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
class FrameworkTest extends TestCase
{
public function testNotFoundHandling(): void
{
$framework = $this->getFrameworkForException(new ResourceNotFoundException());
$response = $framework->handle(new Request());
$this->assertEquals(404, $response->getStatusCode());
}
private function getFrameworkForException($exception): Framework
{
$matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class);
$matcher
->expects($this->once())
->method('match')
->will($this->throwException($exception))
;
$matcher
->expects($this->once())
->method('getContext')
->will($this->returnValue($this->createMock(Routing\RequestContext::class)))
;
$controllerResolver = $this->createMock(ControllerResolverInterface::class);
$argumentResolver = $this->createMock(ArgumentResolverInterface::class);
return new Framework($matcher, $controllerResolver, $argumentResolver);
}
}
Цей тест симулює запит, що не співпадає з жодним маршрутом. Таким чином, метод
match()
повертає виключення ResourceNotFoundException
і ми тестуємо, чи
конвертує наш фреймворк це виключення у відповідь 404.
Виконання цього тесту полягає в простому запуску phpunit
з каталогу
example.com
:
1
$ ./vendor/bin/phpunit
Note
Якщо ви не розумієте, що взагалі відбувається в коді, прочитайте цю документацію PHPUnit про тестові дублі.
Після виконання тесту ви повинні побачити зелений рядок. Якщо ж ні, то у вас є баг або в тесті, або в коді фреймворку!
Додавання модульного тесту для будь-якого викликаного виключення у контролері таке ж просте:
1 2 3 4 5 6 7 8
public function testErrorHandling(): void
{
$framework = $this->getFrameworkForException(new \RuntimeException());
$response = $framework->handle(new Request());
$this->assertEquals(500, $response->getStatusCode());
}
Останнє, але не менш важливе, давайте напишемо тест для випадків, коли у нас є справжня Відповідь:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
// ...
public function testControllerResponse(): void
{
$matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class);
$matcher
->expects($this->once())
->method('match')
->will($this->returnValue([
'_route' => 'is_leap_year/{year}',
'year' => '2000',
'_controller' => [new LeapYearController(), 'index'],
]))
;
$matcher
->expects($this->once())
->method('getContext')
->will($this->returnValue($this->createMock(Routing\RequestContext::class)))
;
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();
$framework = new Framework($matcher, $controllerResolver, $argumentResolver);
$response = $framework->handle(new Request());
$this->assertEquals(200, $response->getStatusCode());
$this->assertStringContainsString('Yep, this is a leap year!', $response->getContent());
}
В цьому тесті ми симулюємо маршрут, який співпадає та повертає простий контролер. Ми перевіряємо, щоб статус відповіді був 200, і щоб її зміст був таким, який ми встановили у контролері.
Щоб переконатися, що ми охопили всі можливі приклади використання, запустіть функцію охоплення PHPUnit тестів (спочатку вам потрібно включити XDebug):
1
$ ./vendor/bin/phpunit --coverage-html=cov/
Відкрийте example.com/cov/src/Simplex/Framework.php.html
у браузере та перевірте, щоб
всі рядки для класу Фреймворку були зеленими (це означає, що вони були відвідані під час
виконання тестів).
Як варіант, ви можете вивести результат напряму у консоль:
1
$ ./vendor/bin/phpunit --coverage-text
Завдяки простому обʼєктно-орієнтованому коду, який ми вже написали, ми змогли написати модульні тести, щоб охопити всі можливі випадки використання нашого фреймворку; тестові дублі гарантували, що ви дійсно тестували наш код, а не код Symfony.
Тепер, коли ми переконалися (знову) у написаному нами коді, ми можемо спокійно подумати про наступну партію функцій, які ми хочемо додати у наш фреймворк.