Внимательные читатели, указывали на некоторые не очевидные, но тем не менее, важные баги в нашем фреймворке. Когда мы создаем фреймворк, мы должны быть уверены, что он ведет себя так, как рекламируется. В противном случае, все приложения созданные на его основе, будут содержать одни и те же ошибки.
Сегодняшняя задача — написать юнит-тесты для нашего фреймворка, с помощью PHPUnit.
Создадим файл конфигурации для PHPUnit phpunit.xml.dist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" syntaxCheck="false" bootstrap="vendor/autoload.php" > <testsuites> <testsuite name="Test Suite"> <directory>./tests</directory> </testsuite> </testsuites> </phpunit> |
Этот файл содержит настройки PHPUnit подходящие для большинства проектов. Интересно, что для загрузки тестов, будет использоваться наш автолоадер, а сами тесты будут храниться в директории tests.
Давайте напишем тест для случая “not found”. Что бы избежать создания всех зависимостей и действительно оттестировать то, что нужно, мы будем использовать заглушки. Их легче создавать, когда мы программируем на уровне интерфейсов, а не конкретных классов. К счастью, Symfony2 предоставляет такие интерфейсы к объектам ядра URL matcher и controller resolver. Воспользуемся этим:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php // framework/src/Simplex/Framework.php namespace Simplex; // ... use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; class Framework { protected $matcher; protected $resolver; public function __construct(UrlMatcherInterface $matcher, ControllerResolverInterface $resolver) { $this->matcher = $matcher; $this->resolver = $resolver; } // ... } |
Теперь можно писать первый тест:
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 |
<?php // framework/tests/Simplex/Tests/FrameworkTest.php namespace Simplex\Tests; use Simplex\Framework; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\ResourceNotFoundException; class FrameworkTest extends \PHPUnit_Framework_TestCase { public function testNotFoundHandling() { $framework = $this->getFrameworkForException(new ResourceNotFoundException()); $response = $framework->handle(new Request()); $this->assertEquals(404, $response->getStatusCode()); } protected function getFrameworkForException($exception) { $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); $matcher ->expects($this->once()) ->method('match') ->will($this->throwException($exception)) ; $resolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface'); return new Framework($matcher, $resolver); } } |
Этот тест эмулирует запрос не соответствующий никакому маршруту. Метод match() выбрасывает ResourceNotFoundException, что позволяет нам протестировать ситуацию, когда наш фреймворк конвертирует это исключение в 404.
Для запуска теста, просто выполните phpunit из корневой директории фреймворка.
1 |
$ phpunit |
Я не буду в деталях разбирать код теста, но если вы не понимаете, что, черт возьми, тут происходит, настоятельно рекомендую вам почитать раздел про заглушки в 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 |
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ControllerResolver; public function testControllerResponse() { $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); $matcher ->expects($this->once()) ->method('match') ->will($this->returnValue(array( '_route' => 'foo', 'name' => 'Fabien', '_controller' => function ($name) { return new Response('Hello '.$name); } ))) ; $resolver = new ControllerResolver(); $framework = new Framework($matcher, $resolver); $response = $framework->handle(new Request()); $this->assertEquals(200, $response->getStatusCode()); $this->assertContains('Hello Fabien', $response->getContent()); } |
В этом тесте мы эмулируем верный маршрут, возвращающий нужный контроллер. Мы проверяем, что статус ответа — 200 и это тот самый контент, который мы ожидаем.
Что бы проверить, что мы покрыли все возможные кейсы, запустим PHPUnit test coverage (XDebug должен быть включен).
1 |
$ phpunit --coverage-html=web/cov/ |
Откройте example.com/cov/src_Simplex_Framework.php.html в браузере, и убедитесь что все строки кода в классе Framework зеленые (это значит, что они были выполнены в процессе запуска тестов).
Благодаря простому объектно-ориентированному коду, мы смогли написать юнит-тесты, покрывающие все возможные кейсы нашего фреймворка, а благодаря заглушкам, мы можем тестировать код фреймворка, а не компоненты Symfony2.
Теперь, когда мы уверены в нашем коде, мы можем спокойно сосредоточится на добавлении новой порции функционала в фреймворк.
К содержанию >>
Оригинал статьи на английском языке >>
Исходный код из статьи >>