Модульное тестирование

Вы могли заметить некоторые маленькие, но тем не менее важные, баги в фреймворке, который мы построили в предыдущей главе. При созданни фреймворка вы должны быть уверены, что он ведёт себя так, как заявлено. Если же нет, то все приложения, основанные на нём, будут иметь однаковые баги. Хорошая новость в том, что когда вы исправляете один баг, вы исправляете кучу других приложеий.

Сегодняшняя миссия - написать модульные тесты для фреймворка, который мы создали используя 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="http://schema.phpunit.de/5.1/phpunit.xsd"
    backupGlobals="false"
    colors="true"
    bootstrap="vendor/autoload.php"
>
    <testsuites>
        <testsuite name="Test Suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./src</directory>
        </whitelist>
    </filter>
</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
22
23
24
// example.com/src/Simplex/Framework.php
namespace Simplex;

// ...

use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;

class Framework
{
    protected $matcher;
    protected $resolver;
    protected $argumentResolver;

    public function __construct(UrlMatcherInterface $matcher, ControllerResolverInterface $resolver, ArgumentResolverInterface $argumentResolver)
    {
        $this->matcher = $matcher;
        $this->resolver = $resolver;
        $this->argumentResolver = $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
43
44
// 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()
    {
        $framework = $this->getFrameworkForException(new ResourceNotFoundException());

        $response = $framework->handle(new Request());

        $this->assertEquals(404, $response->getStatusCode());
    }

    private function getFrameworkForException($exception)
    {
        $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class);
        // используйте getMock() в PHPUnit 5.3 или ниже
        // $matcher = $this->getMock(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
$ phpunit

Note

Если вы не понимаете, что вообще происходит в коде, прочтите эту документацию PHPUnit про тестовые дубли.

После выполнения теста, вы должны увидеть зелёную строку. Если же нет, то у вас есть баг либо в тесте, либо в коде фреймворка!

Добавление модульного теста для любого вызванного исключения в контроллере такое же простое:

1
2
3
4
5
6
7
8
public function testErrorHandling()
{
    $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
34
35
36
37
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
// ...

public function testControllerResponse()
{
    $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class);
    // используйте getMock() в PHPUnit 5.3 или ниже
    // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class);

    $matcher
        ->expects($this->once())
        ->method('match')
        ->will($this->returnValue(array(
            '_route' => 'foo',
            'name' => 'Fabien',
            '_controller' => function ($name) {
                return new Response('Hello '.$name);
            }
        )))
    ;
    $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->assertContains('Hello Fabien', $response->getContent());
}

В этом тесте мы симулируем маршрут, который совпадает и возвращает простой контроллер. Мы проверяем, чтобы статус ответа был 200, и чтобы его содержимое было тем, которое мы установили в контроллере.

Чтобы убедиться, что мы охватили все возможные примеры использования, запустите функцию охвата PHPUnit тестов (вначале вам нужно включить XDebug):

1
$ phpunit --coverage-html=cov/

Откройте example.com/cov/src/Simplex/Framework.php.html в браузере и проверьте, чтобы все строки для класса Фреймворка были зелёными (это значит, что они были посещены при выполнении тестов).

Как вариант, вы можете вывести результат напрямую в консоль:

1
$ phpunit --coverage-text

Благодаря простому объектно-ориентированному коду, который мы уже написали, мы смогли написать модульные тесты, чтобы охватить все возможные случаи использования нашего фреймворка; тестовые дубли гарантировали, что вы действительно тестировали наш код, а не код Symfony.

Теперь, когда мы уверены (опять) в написанном нами коде, мы можем спокойно подумать о следующей партии функций, которые мы хотим добаивть в наш фреймворк.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.