Начиная с самой первой версии Symfony 2, фреймворк предоставляет набор удобных инструментов для выполнения функциональных тестов. Они используют BrowserKit и DomCrawler компоненты для имитации веб-браузера с удобных для разработки API.
WebTestCase
helper
Давайте обновим наши знания, создав небольшой новостной сайт, и соответствующий набор функциональных тестов:
# создадим новый проект composer create-project symfony/skeleton news-website cd news-website/ # добавим некоторые зависимости composer require twig annotations composer require --dev maker tests # запустим встроенный в PHP веб-сервер php -S 127.0.0.1:8000 -t public
Мы готовы писать код. Начнем с написания класса для хранения и добавления новостей:
// src/Repository/NewsRepository.php namespace App\Repository; class NewsRepository { private const NEWS = [ 'week-601' => [ 'slug' => 'week-601', 'title' => 'A week of symfony #601 (2-8 July 2018)', 'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.', ], 'symfony-live-usa-2018' => [ 'slug' => 'symfony-live-usa-2018', 'title' => 'Join us at SymfonyLive USA 2018!', 'body' => 'Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur.' ], ]; public function findAll(): iterable { return array_values(self::NEWS); } public function findOneBySlug(string $slug): ?array { return self::NEWS[$slug] ?? null; } }
Это реализация не очень динамичная, но выполняет свою работу. Теперь нам нужен контроллер и соответствующий Twig шаблон для показа последних новостей сообщества. Мы будем использовать Maker Bundle для их генерации:
./bin/console make:controller News
Отредактируем полученных код в соответствии с нашими требованиями:
// src/Controller/NewsController.php namespace App\Controller; use App\Repository\NewsRepository; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class NewsController extends Controller { private $newsRepository; public function __construct(NewsRepository $newsRepository) { $this->newsRepository = $newsRepository; } /** * @Route("/", name="news_index") */ public function index(): Response { return $this->render('news/index.html.twig', [ 'collection' => $this->newsRepository->findAll(), ]); } /** * @Route("/news/{slug}", name="news_item") */ public function item(string $slug): Response { if (null === $news = $this->newsRepository->findOneBySlug($slug)) { throw $this->createNotFoundException(); } return $this->render('news/item.html.twig', ['item' => $news]); } }
{# templates/news/index.html.twig #} {% extends 'base.html.twig' %} {% block title %}News{% endblock %} {% block body %} {% for item in collection %} <article id="{{ item.slug }}"> <h1><a href="{{ path('news_item', {slug: item.slug}) }}">{{ item.title }}</a></h1> {{ item.body }} </article> {% endfor %} {% endblock %}
{% extends 'base.html.twig' %} {% block title %}{{ item.title }}{% endblock %} {% block body %} <h1>{{ item.title }}</h1> {{ item.body }} {% endblock %}
Благодаря WebTestCase
хелперу, добавить некоторые функциональные тесты для этого вебсайта легко. Во-первых, сгенерируем скелет функциональных тестов:
./bin/console make:functional-test NewsControllerTest
И добавим утверждения, чтобы убедиться, что наш контролер работает правильно:
// tests/NewsControllerTest.php namespace App\Tests; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class NewsControllerTest extends WebTestCase { public function testNews() { $client = static::createClient(); $crawler = $client->request('GET', '/'); $this->assertCount(2, $crawler->filter('h1')); $this->assertSame(['week-601', 'symfony-live-usa-2018'], $crawler->filter('article')->extract('id')); $link = $crawler->selectLink('Присоединяйся к нам на SymfonyLive USA 2018!')->link(); $crawler = $client->click($link); $this->assertSame('Присоединяйся к нам на SymfonyLive USA 2018!', $crawler->filter('h1')->text()); } }
И сейчас, запустим тест:
./bin/phpunit
Всё отлично! Симфони предоставляет очень удобный API для навигации по вебсайту, проверки, что ссылка работает и подтверждения, что ожидаемый контент показывается. Это легко установить, и это очень быстро!
Использование Panther для запуска сценариев в браузере
Однако, WebTestCase не использует реальный браузер. Он симулирует его с помощью компонентов PHP. Он даже не использует HTTP протокол: он создает экземпляры Request HttpFoundation-а, передает их в ядро Симфони, и позволяет утверждать экземпляр HttpFoundation Response, возвращаемый приложением. Что делать, если проблема, которая не дает загрузиться странице, возникает в браузере? Такие проблемы могут быть разными: ссылка скрыта неправильным правилом в CSS, стандартной поведение формы блокируется испорченным javascript скриптом, или, еще хуже, обнаружение браузером уязвимости для безопасности в вашем коде.
Panther дает возможность запускать определенные сценарии прямо в браузере! Он также реализует BrowserKit и DomCrawler API, но под капотом он использует библиотеку Facebook PHP WebDriver. Это означает, что у вас есть выбор — исполнять определенные сценарии браузера в легкой и быстрой PHP реализации (WebTestCase) или в любом современном браузере, через WebDriver протокол для автоматизации браузера, который стал официальной рекомендацией W3C в июне.
Что еще лучше, используя Panther, Вам нужен только Chrome, установленный локально. Не нужно больше ничего устанавливать: ни Selenium (но Panther его тоже поддерживает), ни какой-нибудь другой малоизвестный драйвер браузера или расширение… Фактически, так как Panther теперь зависимость для symfony/test-pack метапакет, вы уже установили Panther не зная этого, когда ввели composer req —dev tests ранее. Вы можете также установить Panther отдельно в любой проект PHP выполнив: composer require symfony/panther .
Давайте подправим некоторые существующие строки нашего тестового проекта:
// tests/NewsControllerTest.php namespace App\Tests; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\Panther\PantherTestCase; -class NewsControllerTest extends WebTestCase +class NewsControllerTest extends PantherTestCase { public function testNews() { - $client = static::createClient(); // Still work, if needed + $client = static::createPantherClient();
Запустим тесты снова:
./bin/phpunit
Все хорошо, но теперь мы убедились, что наш новостной сайт работает правильно в Google Chrome.
Под капотом в Panther может:
- запустить Ваш проект через встроенные в PHP веб-сервер на localhost:9000
- запустить версию Chromedriver, поставляемую с библиотекой, для автоматизации вашего локального Chrome
- выполнить сценарии в браузере установленные в тестах в Chrome headless мод
Если Вы верите только в то, что видите, то попробуйте запустить следующее:
PANTHER_NO_HEADLESS=1 ./bin/phpunit
Как Вы могли увидеть в записи, я добавил некоторые вызовы sleep() чтобы показать, как это работает. Доступ в окно браузера (и в отладчик) также очень полезен для исправления ошибок.
Так как оба инструмента реализуют один и тот же API, Panther также может выполнять сценарии веб-скраппинга, написанные для популярной библиотеки Goutte. В тестовых примерах, Panther предоставляет вам выбор, какой сценарий должен быть выполнен: используя ядро Symfony (когда доступно, static::createClient()), используя Goutte (отправляя реальные HTTP запросы, но не поддерживая Javascript и CSS, static::createGoutteClient()) или используя реальные браузеры (static::createPantherClient()).
Даже если по умолчанию выбран Chrome, Panther может управлять любым браузером, который поддерживает WebDriver протокол. Он также поддерживает сервисы удаленного тестирования через браузер, такие как Selenium Grid (бесплатный), SauceLabs и Browserstack.
Также поддерживается Firefox, используя GeckoDriver.
Тестирование HTML, сгенерированного на стороне клиента
Наш новостной сайт смотрится хорошо, и мы только что доказали, что это работает в Chrome. Но теперь, мы хотим получать отзывы от сообщества о наших частых публикациях. Давайте добавим систему комментариев на наш сайт.
Для этого мы воспользуемся возможностью Symfony 4 и современной веб-платформы: мы будем управлять комментариями через веб API, и отображать их через веб-компоненты и Vue.js. Использование JavaScript для этого позволяет улучшить общую производительность и удобство для пользователей: каждый раз когда мы отправляем новый комментарий, он будет отображен на странице без необходимости полной перегрузки.
Symfony предоставляет официальную интеграцию с API Platform, вероятно самый простой способ создания современных веб API (гипермедиа или/и GraphQL). Установим их:
composer require api
Затем, используйте Maker Bundle снова для создания сущности класса Comment, и предоставления его через точку чтения и записи API:
./bin/console make:entity --api-resource Comment
Это команда интерактивная, и позволяет указать поля для создания. Нам нужно только два: news (слуг новостей) и body (сом комментарий). news типа string (максимальная длина 255 знаков), body типа text. Оба не могут быть null.
Это полный транскрипт взаимодействия с командой:
to stop adding fields): > news Field type (enter ? to see all types) [string]: > Field length [255]: > Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Comment.php to stop adding fields): > body Field type (enter ? to see all types) [string]: > text Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Comment.php
Обновим файл .env вставив значение DATABASE_URI в адрес вашей RDBS и запустим следующую комманду для создания таблицы соответствующей нашей сущности:
./bin/console doctrine:schema:create
Если Вы откроете http://localhost:8000/api, то увидите, что API уже работает и задокументировано.
Мы внесем небольшую модификацию в наш созданный класс Comment. Теперь, API поддерживает GET, POST, PUT, DELETE операции. Это тоже позволено. У нас нет никакого механизма для аутентификации, нам только нужно чтобы наши пользователи могли писать и читатьи комментарии:
/** - * @ApiResource() + * @ApiResource( + * collectionOperations={"post", "get"}, + * itemOperations={"get"} + * )
Затем мы хотим иметь возможность получать комментарии, размещенные к определенной новостной статье. Мы будем использовать сортирующий фильтр для этого:
+ use ApiPlatform\Core\Annotation\ApiFilter; use ApiPlatform\Core\Annotation\ApiResource; + use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; /** * @ORM\Column(type="string", length=255) + * @ApiFilter(SearchFilter::class) */ private $news;
И в конце, добавим определенную валидацию, чтобы быть уверенными, что с нашими комментариями все в порядке:
/** * @ORM\Column(type="string", length=255) + * @Assert\Choice(choices={"week-601", "symfony-live-usa-2018"}) * @ApiFilter(SearchFilter::class) */ private $news; /** * @ORM\Column(type="text") + * @Assert\NotBlank() */ private $body;
Перезагрузим http://localhost:8000/api, чтобы наши изменения автоматически перенеслись в аккаунт.
Создание собственной валидации вместо жесткого списка возможных слугов мы не делали. Оставим это читателям этой статьи в качестве упражнения.
На этом часть php закончилась. Просто, не правда ли? Давайте соединим наш API с Vue.js! Для этого мы будем использовать интеграцию с Vue.js предоставленную Symfony Webpack Encore.
Установим Encore и их интеграцию с Vue.js:
composer require encore yarn install yarn add --dev vue vue-loader@^14 vue-template-compiler
Обновим конфиг Encore для включения загрузчика Vue:
// webpack.config.js Encore // ... + .addEntry('js/comments', './assets/comments/index.js') + .enableVueLoader()
Мы готовы для создания отличного фронтенда! Давайте начнем с компонентом Vue отображать список комментариев и форм для создания новых комментариев:
<!-- assets/comments/CommentSystem.vue --> <template> <div> <ol reversed v-if="comments.length"> <li v-for="comment in comments" :key="comment['@id']">{{ comment.body }}</li> </ol> <p v-else>No comments yet ?</p> <form id="post-comment" @submit.prevent="onSubmit"> <textarea name="new-comment" v-model="newComment" placeholder="Your opinion matters! Send us your comment."></textarea> <input type="submit" :disabled="!newComment"> </form> </div> </template> <script> export default { props: { news: {type: String, required: true} }, methods: { fetchComments() { fetch(`/api/comments?news=${encodeURIComponent(this.news)}`) .then((response) => response.json()) .then((data) => this.comments = data['hydra:member']) ; }, onSubmit() { fetch('/api/comments', { method: 'POST', headers: { 'Accept': 'application/ld+json', 'Content-Type': 'application/ld+json' }, body: JSON.stringify({news: this.news, body: this.newComment}) }) .then(({ok, statusText}) => { if (!ok) { alert(statusText); return; } this.newComment = ''; this.fetchComments(); }) ; } }, data() { return { comments: [], newComment: '', }; }, created() { this.fetchComments(); } } </script>
Это было не так уж сложно, правда?
Далее, создадим точку входа для нашего приложения для комментраиев:
// assets/comments/index.js import Vue from 'vue'; import CommentSystem from './CommentSystem'; new Vue({ el: '#comments', components: {CommentSystem} });
Наконец, укажите ссылку на файл JavaScript и инициализируйте веб-компонент <comment-system> текущим слагом в шаблоне item.html.twig:
{% block body %} <h1>{{ item.title }}</h1> {{ item.body }} + <div id="comments"> + <comment-system news="{{ item.slug }}"></comment-system> + </div> {% endblock %} + {% block javascripts %} + <script src="{{ asset('build/js/comments.js') }}"></script> + {% endblock %}
Создадим транспилированный и миниатюрный JS-файл (вы можете использовать Hot Module Reloading в dev):
yarn encore production
Воу! Благодаря Симфони 4, мы создали веб API и многофункциональное Vue приложение всего из нескольких строк кода. ОК, давайте добавим некоторые тесты для нашей системы комментариев!
Подождите! Комментарии доставляются с помощью AJAX, и отображаются на стороне клиента, в JavaScript. И новые коментарии также добавляются асинхронно используя JS. К сожалению, использовать WebTestCase или Goutte для тестирования нашей новой функции будет невозможно: они написаны на PHP и не поддерживают JavaScript или AJAX.
Не беспокойтесь, Panther готов для тестирования таких веб-приложений. Помните: под капотом он использует реальный веб-браузер!
Давайте протестируем нашу систему комментариев:
namespace App\Tests; use Symfony\Component\Panther\PantherTestCase; class CommentsTest extends PantherTestCase { public function testComments() { $client = static::createPantherClient(); $crawler = $client->request('GET', '/news/symfony-live-usa-2018'); $client->waitFor('#post-comment'); // Wait for the form to appear, it may take some time because it's done in JS $form = $crawler->filter('#post-comment')->form(['new-comment' => 'Symfony is so cool!']); $client->submit($form); $client->waitFor('#comments ol'); // Wait for the comments to appear $this->assertSame(self::$baseUri.'/news/symfony-live-usa-2018', $client->getCurrentURL()); // Assert we're still on the same page $this->assertSame('Symfony is so cool!', $crawler->filter('#comments ol li:first-child')->text()); } }
Для безопасности, в тестовом моде, переменные среды разработки должны быть установлены с помощью phpunit.xml.dist . Обязательно обновите DATABASE_URL. Поставьте ссылку на чистую базу данных, заполненную требуемыми таблицами. Когда база данных готова, запустите тесты:
./bin/phpunit
Благодаря Panther вы можете использовать как свои существующие навыки работы с Symfony, так и прекрасный API BrowserKit для тестирования современных приложений JavaScript.
Экстра способности (скриншоты, инъекции JS)
Но это еще не все, Panther использует реальные веб-браузеры для предоставления функций, которые не поддерживаются компонентом BrowserKit: он может делать скриншоты, ждать появления элементов и выполнять пользовательский JavaScript в контексте выполнения страницы. На самом деле, помимо API BrowserKit, Panther реализует интерфейс Facebook\WebDriver\WebDriver, дающий доступ ко всем функциям PHP Webdriver.
Давайте попробуем это. Обновим предыдущий сценарий теста, чтобы взять скриншот показанной страницы:
$this->assertSame('Symfony is so cool!', $crawler->filter('#comments ol li:first-child')->text()); + $client->takeScreenshot('screen.png');
Panther также предназначен для работы в системах непрерывной интеграции: он поддерживает Travis и AppVeyor из коробки и совместим с Docker!
Для прочтения полной документации или, чтобы поставить звезду проекту, воспользуйтесь репозиторием на GitHub.
Спасибо, открытое программное обеспечение
Panther построен на основе нескольких библиотек FOSS и вдохновлен Nightwatch.js, инструментом тестирования на основе WebDriver для JavaScript, который я использую уже много лет.
Для создания этой библиотеки я использовал несколько зависимостей, и мне пришлось исправить в них некоторые проблемы:
Открытое программное обеспечение — это эффективная экосистема: ты выигрываешь от существующих мощных библиотек для создания инструментов более высокого уровня, и в тоже время ты можешь отдать им должное, улучшив эти библиотеки.
Я также добавляю низкоуровневые улучшения, которые были разработаны во время разработки Panther непосредственно в PHP WebDriver, это:
- хорошая утилита для работы с флажками и переключателями (объединен)
- поддержка W3C-разновидности протокола WebDriver, это единственный протокол, который в настоящее время поддерживается Geckodriver, инструментом для управления безголовым Firefox без Selenium (объединен)
Эти улучшения затем принесут прямую пользу всему сообществу, включая альтернативы Panther, также построенные на основе PHP Webdriver (Laravel Dusk, Codeception, …).
Я хочу поблагодарить всех участников этих проектов, и особенно Ондржея Мачулду, текущего сопровождающего PHP Webdriver, который нашел время, чтобы просмотреть и объединить мои патчи. Особая благодарность также Джорджу С. Боу, который добавил поддержку протокола W3C WebDriver в свою реализацию Perl WebDriver. Когда я наткнулся на него, он очень помог понять, чем протокол отличается от старого протокола Selenium.