Проект на Github
Узнать как установить нужную вам часть руководства, можно в описании к репозиторию по ссылке. (Например, если вы хотите начать с это урока не проходя предыдущий)
Тестирование в Symfony2
PHPUnit стал «де-факто стандартом» для написания тестов в PHP, так что его изучение принесет вам пользу во всех ваших PHP проектах. Давайте также не забывать, что большинство тем, рассматриваемых в данной части, не зависят от языка, и поэтому могут быть применены к другим языкам, которые вы используете.
Совет
Если вы планируете писать свои собственные Open Source бандлы Symfony2, у вас гораздо больше шансов получить хороший отклик от пользователей, если Ваш бандл хорошо протестирован (и задокументирован). Посмотрите на список существующих Symfony2 бандлов, доступных на knpbundles.
Модульное (unit) тестирование
Модульное тестирование проверяет правильную работу отдельных единиц функции при использовании в изоляции. В объектно-ориентированном коде как в Symfony2, модуль это — класс и его методы. Например, мы могли бы писать тесты для классов сущностей Blog и Comment. При написании модульных тестов, тесты должны быть написаны независимо от других тестов, то есть, результат теста Б не должен зависеть от результата А. Модульное тестирование даёт возможность создавать фиктивные объекты, которые позволяют тестировать функции имеющие внешние зависимости. Фиктивные объекты(Моки) позволяют моделировать вызов функции вместо фактического выполнения. Примером этого может быть модульное тестирование класса, который оборачивает внешний API. Класс API может использовать протокол транспортного уровня для осуществления связи с внешним API. Мы могли бы сделать фиктивный метод запроса транспортного уровня, чтобы вернуть результаты, которые мы задали, а не получать на самом деле вернувшиеся с внешнего API. Модульное тестирование не проверяет, правильно ли функционируют компоненты приложения вместе, это показано в следующем разделе.
Функциональное тестирование
Функциональное тестирование проверяет интеграцию различных компонентов приложения, такие как маршрутизация, контроллеры и отображения. Функциональные тесты аналогичны ручным тестам, которые вы могли бы сделать у себя в браузере, например, запрос главной страницы, нажатие на ссылку записи и проверка правильности её отображения. Функциональное тестирование дает вам возможность автоматизировать этот процесс. Symfony2 поставляется в комплекте с целым рядом полезных классов, которые помогают при функциональном тестировании включая Client, который способен делать запросы на страницы, отправлять формы и DOM Crawler который мы можем использовать для обхода Ответа (Response) от клиента.
PHPUnit
Как было указано выше, Symfony2 тесты написаны с использованием PHPUnit. Вам необходимо будет установить PHPUnit для того, чтобы запустить тесты из этой части. Для получения более подробной информации по установке обратитесь к официальной документации на веб-сайте PHPUnit. Для запуска тестов в Symfony2 вам нужно установить PHPUnit 5.4 (требуется PHP 5.6). PHPUnit очень большая библиотека тестирования, в связи с этим будут сделаны ссылки на официальную документацию, где может потребоваться дополнительная информация.
Утверждения (assertions)
Написание тестов является проверкой того, равен ли ожидаемый результат теста, фактическому результату. Существует ряд методов утверждений в PHPUnit, чтобы помочь нам с этой задачей. Некоторые из распространенных методов утверждений, которые мы будем использовать, перечислены ниже.
// Check 1 === 1 is true
$this->assertTrue(1 === 1);
// Check 1 === 2 is false
$this->assertFalse(1 === 2);
// Check 'Hello' equals 'Hello'
$this->assertEquals('Hello', 'Hello');
// Check array has key 'language'
$this->assertArrayHasKey('language', array('language' => 'php', 'size' => '1024'));
// Check array contains value 'php'
$this->assertContains('php', array('php', 'ruby', 'c++', 'JavaScript'));
Полный список утверждений(assertions) доступен в документации PHPUnit
Запуск тестов Symfony2
Перед тем, как приступить к написанию нескольких тестов, давайте посмотрим на то, как мы запускаем тесты в Symfony2. РНРUnit может использовать файл конфигурации. В нашем проекте Symfony2 этот файл находится app/phpunit.xml.dist. Так как этот файл с суффиксом .dist, вам необходимо скопировать его содержимое в файл с именем app/phpunit.xml.
Совет
Если вы используете VCS, такую как Git, вы должны добавить файл app/phpunit.xml в список игнорирования.
Если вы посмотрите на содержимое файла конфигурации PHPUnit вы увидите следующее.
<!-- app/phpunit.xml -->
<testsuites>
<testsuite name="Project Test Suite">
<directory>../src/*/*Bundle/Tests</directory>
<directory>../src/*/Bundle/*Bundle/Tests</directory>
</testsuite>
</testsuites>
Эти параметры настраивают некоторые каталоги, которые являются частью нашего тестового набора. При запуске PHPUnit будет искать тесты для запуска в указанных выше каталогах. Вы также можете передать дополнительные аргументы из командной строки для запуска тестов PHPUnit в определенных каталогах. Вы увидите, как добиться этого позже.
Совет
Для получения дополнительной информации о настройке PHPUnit с помощью файла XML смотрите документацию PHPUnit.
Запуск текущих тестов
Так как мы использовали одну из команд Symfony2 генератора для создания Blogger BlogBundle в первой части, она также создала тест контроллера для класса Default Controller. Мы можем выполнить этот тест, запустив следующую команду из корневого каталога проекта. -c опция указывает, что PHPUnit должен загрузить свою конфигурацию из каталога app.
$ phpunit -c app
После того, как тестирование завершится, вы должны быть уведомлены о том, что тест не удался. Если вы посмотрите на класс DefaultControllerTest, расположенного src/Blogger/BlogBundle/Tests/Controller/DefaultControllerTest.php вы увидите
<?php
namespace BloggerBlogBundleTestsController;
use SymfonyBundleFrameworkBundleTestWebTestCase;
class DefaultControllerTest extends WebTestCase
{
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
$this->assertContains('Hello World', $client->getResponse()->getContent());
}
}
Это функциональный тест для класса Default Controller, который сгенерировал Symfony2. Если вы помните в части 1, у этого контроллера был метод, который обрабатывал запрос /, по этому адресу у нас выводился шаблон с содержанием “Hello World”. Так как мы удалили этот контроллер и у шаблона по этому маршруту совсем другое содержание, вышеупомянутый тест терпит неудачу.
Функциональное тестирование является большой частью этой главы и будет рассмотрено в деталях позже.
Так как был удален класс Default Controller, вы можете удалить файл src/Blogger/BlogBundle/Tests/Controller/DefaultControllerTest.php
Модульное тестирование
Как объяснялось ранее, модульное тестирование касается тестирования отдельных единиц вашего приложения в изоляции. При написании модульных тестов рекомендуется дублировать структуру бандла в папке тестов. Например, если вы хотите, протестировать сущность класса Blog, расположенную в src/Blogger/BlogBundle/Entity/Blog.php тестовый файл будет находиться в src/Blogger/BlogBundle/Tests/Entity/BlogTest.php. Пример будет выглядеть
src/Blogger/BlogBundle/
Entity/
Blog.php
Comment.php
Controller/
PageController.php
Twig/
Extensions/
BloggerBlogExtension.php
Tests/
Entity/
BlogTest.php
CommentTest.php
Controller/
PageControllerTest.php
Twig/
Extensions/
BloggerBlogExtensionTest.php
Обратите внимание на то, что каждый из тестовых файлов имеет суффикс Test.
Тестирование сущности Blog — метод slugify
Мы начнем тестирование метода slugify в сущности Blog. Давайте напишем несколько тестов, чтобы убедиться, что этот метод работает правильно. Создайте новый файл, расположенный в
<?php
// src/Blogger/BlogBundle/Tests/Entity/BlogTest.php
namespace BloggerBlogBundleTestsEntity;
use BloggerBlogBundleEntityBlog;
class BlogTest extends PHPUnit_Framework_TestCase
{
}
Мы создали тестовый класс для сущности Blog. Обратите внимание, что расположение файла соответствует структуре папок, упомянутой выше. Класс Blog Test расширяет базовый класс PHPUnit PHPUnit_Framework_TestCase. Все тесты, которые Вы пишете для PHPUnit будут потомками этого класса. Вы помните из предыдущих частей, что должен быть помещен перед именем класса PHPUnit_Framework_TestCase так как класс объявлен в общем пространстве имен PHP.
Теперь у нас есть скелет класса для тестов сущности Blog, давайте напишем тест. Тесты в PHPUnit являются методами класса Test с префиксом test, например testSlugify(). Обновите BlogTest, расположенный
// src/Blogger/BlogBundle/Tests/Entity/BlogTest.php
// ..
class BlogTest extends PHPUnit_Framework_TestCase
{
public function testSlugify()
{
$blog = new Blog();
$this->assertEquals('hello-world', $blog->slugify('Hello World'));
}
}
Это очень простой тест. Он создает новую сущность Blog и запускает assertEquals() для результата метода slugify. Метод assertEquals() принимает 2 обязательных аргумента, ожидаемый результат и фактический результат. Может быть передан необязательный третий аргумент, чтобы вывести сообщение, когда тест терпит неудачу.
Давайте запустим наш новый unit тест. Введите следующую команду в консоли.
$ phpunit -c app
Вы должны увидеть следующее.
PHPUnit 5.4.6 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 348 ms, Memory: 13.25MB
OK (1 test, 1 assertion)
Вывод РНРunit очень прост, сначала, показывается некоторая информация о PHPUnit и количество каждого запущенного теста, в нашем случае мы имеем только 1 тест. В конце он информирует нас о результатах теста. Для нашего BlogTest мы запустили только 1 тест с 1 утверждением. Если у вас настроен вывод цветов в вашей командной строке вы также увидите, что последняя строка закрашена зеленым цветом, показывая, что все в порядке ОК. Давайте обновим метод testSlugify() чтобы посмотреть, что произойдёт, когда тест потерпит неудачу.
// src/Blogger/BlogBundle/Tests/Entity/BlogTest.php
// ..
public function testSlugify()
{
$blog = new Blog();
$this->assertEquals('hello-world', $blog->slugify('Hello World'));
$this->assertEquals('a day with symfony2', $blog->slugify('A Day With Symfony2'));
}
Перезапустите модульный тест как раньше. Будет выведено
PHPUnit 5.4.6 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 340 ms, Memory: 13.25MB
There was 1 failure:
1) BloggerBlogBundleTestsEntityBlogTest::testSlugify
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'a day with symfony2'
+'a-day-with-symfony2'
D:localsymfony-blogsrcBloggerBlogBundleTestsEntityBlogTest.php:15
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.
Вывод немного больше в этот раз. Символ F говорит нам о том, что тест не пройден. Вы также увидите символ E если ваш тест будет содержать ошибки. Далее РНРUnit уведомляет нас о деталях неудач, в данном случае одной. Мы можем видеть что метод BloggerBlogBundleTestsEntityBlogTest::testSlugify не удался, поскольку ожидаемые и фактические значения различны. Если у вас есть вывод цветов в вашей командной строке вы также увидите, что последняя строка закрашена красным цветом, показывая, что в тесте есть ошибки. Исправьте метод testSlugify() так, чтобы он был успешно выполнен.
// src/Blogger/BlogBundle/Tests/Entity/BlogTest.php
// ..
public function testSlugify()
{
$blog = new Blog();
$this->assertEquals('hello-world', $blog->slugify('Hello World'));
$this->assertEquals('a-day-with-symfony2', $blog->slugify('A Day With Symfony2'));
}
Перед тем как пойти дальше добавьте еще несколько тестов
// src/Blogger/BlogBundle/Tests/Entity/BlogTest.php
// ..
public function testSlugify()
{
$blog = new Blog();
$this->assertEquals('hello-world', $blog->slugify('Hello World'));
$this->assertEquals('a-day-with-symfony2', $blog->slugify('A Day With Symfony2'));
$this->assertEquals('hello-world', $blog->slugify('Hello world'));
$this->assertEquals('symblog', $blog->slugify('symblog '));
$this->assertEquals('symblog', $blog->slugify(' symblog'));
}
Теперь, когда мы протестировали метод slugify, мы должны убедиться что Blog $slug правильно установлен, когда обновляется Blog $title. Добавьте следующие методы в файл
// src/Blogger/BlogBundle/Tests/Entity/BlogTest.php
// ..
public function testSetSlug()
{
$blog = new Blog();
$blog->setSlug('Symfony2 Blog');
$this->assertEquals('symfony2-blog', $blog->getSlug());
}
public function testSetTitle()
{
$blog = new Blog();
$blog->setTitle('Hello World');
$this->assertEquals('hello-world', $blog->getSlug());
}
Мы начали с тестирования метода setSlug, чтобы убедиться, что элемент $slug правильно добавляет slug при обновлении. Далее мы проверяем, что $slug правильно обновляется, когда метод setTitle вызывается сущностью Blog.
Запустите тесты для проверки работоспособности сущности Blog.
Тестирование расширения Twig
В предыдущей части мы создали расширение Twig для преобразования DateTime в строку, чтобы определить время, прошедшее с момента публикации комментария. Создайте новый файл
<?php
// src/Blogger/BlogBundle/Tests/Twig/Extensions/BloggerBlogExtensionTest.php
namespace BloggerBlogBundleTestsTwigExtensions;
use BloggerBlogBundleTwigExtensionsBloggerBlogExtension;
class BloggerBlogExtensionTest extends PHPUnit_Framework_TestCase
{
public function testCreatedAgo()
{
$blog = new BloggerBlogExtension();
$this->assertEquals("0 seconds ago", $blog->createdAgo(new DateTime()));
$this->assertEquals("34 seconds ago", $blog->createdAgo($this->getDateTime(-34)));
$this->assertEquals("1 minute ago", $blog->createdAgo($this->getDateTime(-60)));
$this->assertEquals("2 minutes ago", $blog->createdAgo($this->getDateTime(-120)));
$this->assertEquals("1 hour ago", $blog->createdAgo($this->getDateTime(-3600)));
$this->assertEquals("1 hour ago", $blog->createdAgo($this->getDateTime(-3601)));
$this->assertEquals("2 hours ago", $blog->createdAgo($this->getDateTime(-7200)));
// Cannot create time in the future
$this->setExpectedException('InvalidArgumentException');
$blog->createdAgo($this->getDateTime(60));
}
protected function getDateTime($delta)
{
return new DateTime(date("Y-m-d H:i:s", time()+$delta));
}
}
Класс настроен так же, как и раньше, создавая метод testCreatedAgo() для тестирования расширения Twig. Мы ввели еще один метод PHPUnit в этом тесте setExpectedException(). Мы знаем, что метод createdAgo расширения Twig не может обрабатывать даты в будущем и будет передан в Exception. метод getDateTime() вспомогательный, для создания экземпляра DateTime. Обратите внимание на то, что нет префикса test, так РНРUnit не будет пытаться выполнить его в качестве теста. Откройте консоль и запустите тесты для этого файла. Мы могли бы просто запустить тест, как и раньше, но мы можем также указать PHPUnit выполнять тесты только для определенной папки (и вложенных папок) или файла. Введите следующую команду:
$ phpunit -c app src/Blogger/BlogBundle/Tests/Twig/Extensions/BloggerBlogExtensionTest.php
Это запустит тесты только для файла BloggerBlogExtensionTest. PHPUnit сообщит нам, что тест не удался. Вывод показан ниже.
1) BloggerBlogBundleTestsTwigExtensionsBloggerBlogExtensionTest::testCreatedAgo
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'0 seconds ago'
+'0 second ago'
//..
Мы ожидали, что первое утверждение вернётся 0 секунд назад, но оно не вернулось так как второе слово не было множественного числа. Давайте обновим расширение Twig чтобы исправить это
<?php
// src/Blogger/BlogBundle/Twig/Extensions/BloggerBlogBundle.php
namespace BloggerBlogBundleTwigExtensions;
class BloggerBlogExtension extends Twig_Extension
{
// ..
public function createdAgo(DateTime $dateTime)
{
// ..
if ($delta < 60)
{
// Seconds
$time = $delta;
$duration = $time . " second" . (($time === 0 || $time > 1) ? "s" : "") . " ago";
}
// ..
}
// ..
}
Перезапустите Unit тесты. Вы должны увидеть, что первое утверждение отображается правильно, но тест до сих пор не удается. Давайте рассмотрим следующий вывод.
1) BloggerBlogBundleTestsTwigExtensionsBloggerBlogExtensionTest::testCreatedAgo
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'1 hour ago'
+'60 minutes ago'
Мы видим, что не удается пятое утверждение. Глядя на тест, мы можем видеть, что Расширение Twig функционировало неправильно. Должно было быть возвращено 1 час назад, но вместо этого возвращено 60 минут назад. Если мы рассмотрим код в BloggerBlogExtension Twig мы увидим причину. Мы сравниваем время, которое может быть включено, то есть, мы используем <= вместо <. Обновите расширение Twig чтобы это исправить
<?php
// src/Blogger/BlogBundle/Twig/Extensions/BloggerBlogBundle.php
namespace BloggerBlogBundleTwigExtensions;
class BloggerBlogExtension extends Twig_Extension
{
// ..
public function createdAgo(DateTime $dateTime)
{
// ..
else if ($delta < 3600)
{
// Mins
$time = floor($delta / 60);
$duration = $time . " minute" . (($time > 1) ? "s" : "") . " ago";
}
else if ($delta < 86400)
{
// Hours
$time = floor($delta / 3600);
$duration = $time . " hour" . (($time > 1) ? "s" : "") . " ago";
}
// ..
}
// ..
}
Теперь вновь запустим все тесты с помощью следующей команды.
$ phpunit -c app
Это запустит все наши тесты и покажет, что все тесты успешно пройдены. Хотя мы написали только небольшое количество модульных тестов вы должны получить чувство того, насколько мощным и важным является тестирование при написании кода. Тестирование также помогает добавлять любые будущие функциональные возможности к проекту, не ломая предыдущие характеристики. На этом мы завершим рассмотрение модульного тестирования.
Совет
Попробуйте добавить ваши собственные модульные тесты.
Функциональное тестирование
Мы написали несколько модульных тестов, давайте перейдем к тестированию нескольких компонентов вместе. Первый раздел функционального тестирования будет включать в себя имитацию запросов браузера для тестирования сгенерированных ответов.
Тестирование страницы About
Мы начнем с тестирования класса PageController для страницы About. Так как страница является очень простой, это хорошее место для начала. Создайте новый файл
<?php
// src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
namespace BloggerBlogBundleTestsController;
use SymfonyBundleFrameworkBundleTestWebTestCase;
class PageControllerTest extends WebTestCase
{
public function testAbout()
{
$client = static::createClient();
$crawler = $client->request('GET', '/about');
$this->assertEquals(1, $crawler->filter('h1:contains("About symblog")')->count());
}
}
Мы увидим, что тест контроллера очень похож на класс DefaultControllerTest. Тестирование страницы about, проверяет строку About symblog которая присутствует в сгенерированном HTML, а именно внутри тега H1. Класс PageControllerTest не расширяет PHPUnit_Framework_TestCase, как мы видели в примере с модульным тестированием, вместо этого он расширяет класс WebTestCase. Этот класс является частью Symfony2 FrameworkBundle.
Как было объяснено выше тестовые классы PHPUnit должны расширять PHPUnit_Framework_TestCase но, когда дополнительные или общие функциональные возможности требуются в нескольких тестах полезно инкапсулировать это в своем собственном классе и сделать так чтобы ваш тестовые классы его расширяли. WebTestCase делает именно это, он предоставляет несколько полезных методов для выполнения функциональных тестов в Symfony2. Посмотрите на файл WebTestCase, расположенного vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php, вы увидите, что этот класс фактически является расширением класса KernelTestCase.
// vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php
abstract class WebTestCase extends KernelTestCase
{
// ..
}
Если вы посмотрите на метод createClient() в классе WebTestCase вы можете увидеть, что он создает экземпляр Symfony2 Kernel. Следуя методам дальше вы также заметите, что окружение установлено в test (если не переопределено в качестве одного из аргументов createClient()). Это тестовое окружение о котором мы говорили в предыдущей части.
Посмотрев на наш тестовый класс мы можем видеть что для получения и запуска теста вызывается метод createClient(). Затем мы вызываем метод request() на стороне клиента для имитации GET HTTP-запроса браузера по URL /about (это будет точно тем же, если бы вы посетили страницу http://localhost:8000/about в браузере). Запрос дает нам объект Crawler, который содержит ответ. Класс Crawler очень полезен, поскольку он позволяет нам переместить возвращенный HTML. Мы используем экземпляр Crawler для проверки, что тег H1 в ответе HTML содержит слова About symblog. Вы заметите, что даже если мы расширяем класс WebTestCase мы до сих пор используем метод Assert, как и раньше (помните, класс PageControllerTest по-прежнему является потомком классаPHPUnit_Framework_TestCase).
Давайте запустим PageControllerTest, следующей командой. При написании тестов её полезно использовать для выполнения тестов только для файла, в котором вы сейчас работаете.
$ phpunit -c app/ src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
Должно быть выведено сообщение OK (1 test, 1 assertion) давая нам знать, что 1 тест (testAbout() был запущен), с 1 утверждением (assertEquals()).
Попробуйте изменить строку About symblog на Contact, а затем повторно запустить тест. Тест потерпит неудачу, поскольку Contact не будет найден, в результате чего assertEquals приравняется к false.
Верните строку About symblog обратно перед тем как мы продолжим.
Экземпляр Crawler позволяет просматривать HTML или XML документы, (что означает, что Crawler будет работать только с ответами, которые возвращают HTML или XML). Мы можем использовать Crawler для обхода сгенерированного ответа, используя такие методы, как filter(), first(), last() и parents(). Если прежде вы использовали jQuery, вы должны чувствовать себя как дома с классом Crawler. Полный список поддерживаемых методов обхода Crawler можно найти в главе Testing книги Symfony2.
Домашняя страница
В то время как тест для страницы About, был простым, он обозначил основные принципы функционального тестирования страницы сайта.
Создание клиента
Запрос страницы
Проверка ответа
Это простой обзор процесса, на самом деле существует ещё целый ряд других шагов, которые мы могли бы сделать такие, как нажатие на ссылки, заполнение и отправка формы.
Давайте создадим метод для тестирования домашней страницы. Мы знаем, что главная страница доступна по маршруту / и что она должна отображать последние записи в блоге. Добавьте новый метод testIndex() в класс PageControllerTest, расположенный
// src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
// Check there are some blog entries on the page
$this->assertTrue($crawler->filter('article.blog')->count() > 0);
}
Вы можете увидеть такие же шаги, как и с тестом для страницы about. Запустите тест для того, чтобы убедиться в том, что все работает корректно.
$ phpunit -c app/ src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
Давайте теперь пойдем немного дальше. Часть функционального тестирования включает в себя способность повторить то, что пользователь будет делать на сайте. Для того, чтобы перемещаться по страницам вашего сайта, пользователи нажимают на ссылки. Давайте сейчас смоделируем это действие, чтобы проверить правильность ссылок на страницу записи, когда происходит нажатие на заголовок.
public function testIndex()
{
//…
// Find the first link, get the title, ensure this is loaded on the next page
$blogLink = $crawler->filter('article.blog h2 a')->first();
$blogTitle = $blogLink->text();
$crawler = $client->click($blogLink->link());
// Check the h2 has the blog title in it
$this->assertEquals(1, $crawler->filter('h2:contains("'. $blogTitle .'")')->count());
}
Первое, что мы сделаем это используем Crawler, чтобы извлечь текст из первой ссылки названия записи. Это делается с помощью фильтра article.blog h2 a. Этот фильтр используется чтобы вернуть тег a в теге H2 article.blog статьи. Чтобы лучше это понять, взгляните на разметку, используемой на домашней странице для отображения записи.
<article class="blog">
<div class="date"><time datetime="2016-06-17T14:23:55+03:00">Friday, June 17, 2016</time></div>
<header>
<h2><a href="/1/a-day-with-symfony2">A day with Symfony2</a></h2>
</header>
//..
</article>
Вы можете увидеть структуру фильтра article.blog h2 a в разметке домашней страницы. Вы также заметите, что существует несколько в разметке, это означает что фильтр Crawler возвращает коллекции. Так как мы хотим, только первую ссылку, мы используем first() метод в коллекции. Метод click() принимает объект ссылки и возвращает ответ в экземпляре Crawler. Вы заметите, что объект Crawler является ключевым элементом для функционального тестирования.
Теперь объект Crawler содержит ответ страницы блога. Нам нужно протестировать, что ссылка перенаправляет нас на нужную страницу. Мы можем использовать значение $blogTitle которое мы извлекли ранее, чтобы проверить заголовок в ответе.
Запустите тесты, чтобы убедиться, что навигация между домашней страницей и страницей записей работает правильно.
$ phpunit -c app/ src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
Теперь у вас есть понимание того, как перемещаться по страницам сайта при функциональном тестировании, давайте перейдем к тестированию форм.
Тестирование страницы контактов
Пользователи symblog могут отправлять контактные запросы, заполнив форму на странице контактов http://localhost:8000/contact. Давайте протестируем, что запросы работают правильно. Сначала мы должны определить, что должно произойти, когда форма успешно отправлена (успешно отправлена в данном случае означает, без ошибок форме).
Переход на страницу контактов
Заполнение формы данными
Отправка формы
Проверка того что письмо отправлено
Проверка уведомления об успешной отправке
До этого момента мы исследовали достаточно чтобы бы выполнить только шаги 1 и 5. Теперь мы рассмотрим, как протестировать 3 остальных шага.
Добавьте новый метод testContact() в класс PageControllerTest, расположенного
// src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
public function testContact()
{
$client = static::createClient();
$crawler = $client->request('GET', '/contact');
$this->assertEquals(1, $crawler->filter('h1:contains("Contact symblog")')->count());
// Select based on button value, or id or name for buttons
$form = $crawler->selectButton('Submit')->form();
$form['contact[name]'] = 'name';
$form['contact[email]'] = 'email@email.com';
$form['contact[subject]'] = 'Subject';
$form['contact[body]'] = 'The comment body must be at least 50 characters long as there is a validation constrain on the Enquiry entity';
$crawler = $client->submit($form);
$this->assertEquals(1, $crawler->filter('.blogger-notice:contains("Your contact enquiry was successfully sent. Thank you!")')->count());
}
Мы начнём с запроса на url /contact и проверим, что страница содержит правильный заголовок H1. Далее мы используем Crawler чтобы выбрать кнопку отправки формы. Причиной того что мы выбираем кнопку, а не форму является то, что форма может содержать несколько кнопок, которые мы можем захотеть нажать независимо друг от друга. От выбранной кнопки мы можем получить форму. Мы можем установить значения формы, используя массив []. Наконец форма передается на клиентский submit() метод чтобы отправить форму. Как обычно, назад мы получаем экземпляр Crawler. Используя ответ Crawler мы проверяем, что flash сообщение присутствует в возвращенном ответе. Выполните тест, чтобы проверить что все функционирует нормально.
$ phpunit -c app/ src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
Тест не удался. Мы получим следующее сообщение PHPUnit
1) BloggerBlogBundleTestsControllerPageControllerTest::testContact
Failed asserting that 0 matches expected 1.
D:localsymfony-blogsrcBloggerBlogBundleTestsControllerPageControllerTest.php:55
FAILURES!
Tests: 3, Assertions: 5, Failures: 1.
Вывод информирует нас о том, что flash сообщение не может быть найдено в ответе после отправки формы. Это произошло потому, что, когда мы находимся в тестовом окружении, перенаправления не выполняются. Когда форма успешно проверена в классе PageController происходит перенаправление. Это перенаправление не происходит; Нам нужно явно указать, что оно должно быть соблюдено. Причина, по которой перенаправления не выполняются, проста это происходит в связи с тем, что вы можете захотеть в первую очередь проверить текущий ответ. Мы увидим это позже, когда будем проверять отправилось ли сообщение. Обновите класс PageControllerTest для установки перенаправления.
// src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
public function testContact()
{
// ..
$crawler = $client->submit($form);
// Need to follow redirect
$crawler = $client->followRedirect();
$this->assertEquals(1, $crawler->filter('.blogger-notice:contains("Your contact enquiry was successfully sent. Thank you!")')->count());
}
Сейчас если мы запустим тесты PHPUnit мы должны их пройти. Давайте теперь посмотрим на заключительный этап проверки процесса отправки контактной формы, шаг 4, проверка того что почта была отправлена. Мы уже знаем, что почта не будет доставлена в test окружении из-за следующей конфигурации.
# app/config/config_test.yml
swiftmailer:
disable_delivery: true
Мы можем проверить факт того что письма были отправлены с использованием информации, собранной с помощью веб-профайлера. Это то место где важно не перенаправлять клиента. Проверка профайлера должна быть сделана до того, как произойдёт перенаправление, в противном случае информация в профайлере будет потеряна.
// src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
public function testContact()
{
// ..
$crawler = $client->submit($form);
// Check email has been sent
if ($profile = $client->getProfile())
{
$swiftMailerProfiler = $profile->getCollector('swiftmailer');
// Only 1 message should have been sent
$this->assertEquals(1, $swiftMailerProfiler->getMessageCount());
// Get the first message
$messages = $swiftMailerProfiler->getMessages();
$message = array_shift($messages);
$symblogEmail = $client->getContainer()->getParameter('blogger_blog.emails.contact_email');
// Check message is being sent to correct address
$this->assertArrayHasKey($symblogEmail, $message->getTo());
}
// Need to follow redirect
$crawler = $client->followRedirect();
$this->assertTrue($crawler->filter('.blogger-notice:contains("Your contact enquiry was successfully sent. Thank you!")')->count() > 0);
}
После отправки формы мы проверяем, доступен ли профайлер, так как это может быть отключено с помощью конфигурации для текущего окружения.
Совет
Помните, что тесты не должны выполняться в тестовой среде, они должны быть запущены в рабочем окружении, где вещи, такие как профайлер не будут доступны.
Если мы сможем получить профайлер мы делаем запрос для извлечения swiftmailer коллектора. swiftmailer коллектор работает за кулисами, чтобы собирать информацию о том, как используется служба электронной почты. Мы можем использовать это, чтобы получить информацию о том, какие письма были отправлены.
Далее мы используем метод getMessageCount(), чтобы проверить, что было отправлено 1 письмо. Этого будет достаточно, чтобы гарантировать, что почта была отправлена, но это не проверяет, то что почта будет отправлена в нужное место.
Теперь перезапустим тесты и убедимся, что всё работает нормально.
$ phpunit -c app/ src/Blogger/BlogBundle/Tests/Controller/PageControllerTest.php
Тестирование добавления комментариев
Давайте теперь используем знания, полученные из предыдущих тестов на странице контактной информации, чтобы проверить процесс добавления комментариев к записи. Опять же мы наметим, что должно произойти, когда форма будет успешно отправлена.
Переход на страницу записи
Заполнение формы значениями
Отправка формы
Проверка того что комментарий был добавлен в самый конец всех комментариев
Также проверка боковой панели, блока Последние комментарии (что комментарий первый в списке)
Создайте новый файл, расположенный в
<?php
// src/Blogger/BlogBundle/Tests/Controller/BlogControllerTest.php
namespace BloggerBlogBundleTestsController;
use SymfonyBundleFrameworkBundleTestWebTestCase;
class BlogControllerTest extends WebTestCase
{
public function testAddBlogComment()
{
$client = static::createClient();
$crawler = $client->request('GET', '/1/a-day-with-symfony');
$this->assertEquals(1, $crawler->filter('h2:contains("A day with Symfony2")')->count());
// Select based on button value, or id or name for buttons
$form = $crawler->selectButton('Submit')->form();
$crawler = $client->submit($form, array(
'blogger_blogbundle_commenttype[user]' => 'name',
'blogger_blogbundle_commenttype[comment]' => 'comment',
));
// Need to follow redirect
$crawler = $client->followRedirect();
// Check comment is now displaying on page, as the last entry. This ensure comments
// are posted in order of oldest to newest
$articleCrawler = $crawler->filter('section .previous-comments article')->last();
$this->assertEquals('name', $articleCrawler->filter('header span.highlight')->text());
$this->assertEquals('comment', $articleCrawler->filter('p')->last()->text());
// Check the sidebar to ensure latest comments are display and there is 10 of them
$this->assertEquals(10, $crawler->filter('aside.sidebar section')->last()
->filter('article')->count()
);
$this->assertEquals('name', $crawler->filter('aside.sidebar section')->last()
->filter('article')->first()
->filter('header span.highlight')->text()
);
}
}
Прежде чем мы начнем исследовать код, запустите тесты для этого файла, чтобы убедиться, что все работает правильно.
$ phpunit -c app/ src/Blogger/BlogBundle/Tests/Controller/BlogControllerTest.php
PHPUnit должен сообщить вам, что 1 тест был успешно выполнен. Глядя на код для testAddBlogComment() мы можем видеть создание клиента, запрос страницы и её проверку. Затем переходим к получению формы Добавления комментария и отправки формы. Способ, которым мы заполнили значения формы немного отличается от предыдущей версии. На этот раз мы используем 2-й аргумент клиента submit() метод добавления значений формы.
Совет
Мы могли бы также использовать объектно-ориентированный интерфейс для установки значений полей формы. Примеры показаны ниже.
// Tick a checkbox $form['show_emal']->tick(); // Select an option or a radio $form['gender']->select('Male');
После отправки формы, мы запрашиваем клиент который должен следить за перенаправлением, чтобы мы могли проверить ответ. Мы снова используем Crawler, чтобы получить последний комментарий в блоге, который должен быть тем который мы только что отправили. Наконец мы проверяем последние комментарии в боковой панели, чтобы проверить, что комментарий является первым в списке.
Репозиторий Blog
В последней части функционального тестирования мы исследуем тестирование Doctrine 2 Репозитория. Создайте новый файл
<?php
// src/Blogger/BlogBundle/Tests/Repository/BlogRepositoryTest.php
namespace BloggerBlogBundleTestsRepository;
use BloggerBlogBundleEntityRepositoryBlogRepository;
use SymfonyBundleFrameworkBundleTestWebTestCase;
class BlogRepositoryTest extends WebTestCase
{
/**
* @var BloggerBlogBundleEntityRepositoryBlogRepository
*/
private $blogRepository;
public function setUp()
{
$kernel = static::createKernel();
$kernel->boot();
$this->blogRepository = $kernel->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('BloggerBlogBundle:Blog');
}
public function testGetTags()
{
$tags = $this->blogRepository->getTags();
$this->assertTrue(count($tags) > 1);
$this->assertContains('symblog', $tags);
}
public function testGetTagWeights()
{
$tagsWeight = $this->blogRepository->getTagWeights(
array('php', 'code', 'code', 'symblog', 'blog')
);
$this->assertTrue(count($tagsWeight) > 1);
// Test case where count is over max weight of 5
$tagsWeight = $this->blogRepository->getTagWeights(
array_fill(0, 10, 'php')
);
$this->assertTrue(count($tagsWeight) >= 1);
// Test case with multiple counts over max weight of 5
$tagsWeight = $this->blogRepository->getTagWeights(
array_merge(array_fill(0, 10, 'php'), array_fill(0, 2, 'html'), array_fill(0, 6, 'js'))
);
$this->assertEquals(5, $tagsWeight['php']);
$this->assertEquals(3, $tagsWeight['js']);
$this->assertEquals(1, $tagsWeight['html']);
// Test empty case
$tagsWeight = $this->blogRepository->getTagWeights(array());
$this->assertEmpty($tagsWeight);
}
}
Поскольку мы хотим выполнить тесты, которые требуют правильного подключения к базе данных мы снова расширим WebTestCase, так как это позволяет загружать Symfony2 Kernel. Запустите тест для этого файла с помощью следующей команды.
$ phpunit -c app/ src/Blogger/BlogBundle/Tests/Repository/BlogRepositoryTest.php
Покрытие кода
Покрытие кода дает нам понимание того, какие части кода выполняются, когда запускаются тесты. С помощью этого мы можем увидеть части нашего кода, которые не покрыты тестами и определить, нужно ли писать тест для них.
Для вывода покрытия кода для вашего приложения введите в консоли
$ phpunit --coverage-html ./phpunit-report -c app/
Учтите, чтобы команда сработала, необходим активированный xdebug в php.
Это выведет анализ покрытия кода в папку phpunit-report в корне проекта. Откройте файл index.html из этой папки в вашем браузере, чтобы увидеть результат анализа.
Вывод
Мы рассмотрели ряд ключевых областей в тестировании. Исследовали как модульное так и функциональное тестирование, чтобы убедиться в том, что наш сайт функционирует нормально. Мы увидели, как симулировать запросы браузера и как использовать класс Symfony2 Crawler для проверки ответа этих запросов.
Источники и вспомогательные материалы:
https://symfony.com/
http://tutorial.symblog.co.uk/
https://phpunit.de/
Всем спасибо за внимание и замечания сделанные по проекту, если у вас возникли сложности или вопросы, отписывайтесь в комментарии или личные сообщения, добавляйтесь в друзья.
Часть 1 — Конфигурация Symfony2 и шаблонов
Часть 2 — Страница с контактной информацией: валидаторы, формы и электронная почта
Часть 3 — Doctrine 2 и Фикстуры данных
Часть 4 — Модель комментариев, Репозиторий и Миграции Doctrine 2
Часть 5 — Twig расширения, Боковая панель(sidebar) и Assetic
Также, если Вам понравилось руководство вы можете поставить звезду репозиторию проекта или подписаться. Спасибо.
Автор: antoscenco-vladimir