Квест → Как хакнуть форму
Прошли: 77
С самой первой версии Symfony 2, фреймворк предоставлял набор удобных инструментов для создания функциональных тестов. Они используют компоненты BrowserKit и DomCrawler для имитации веб-браузера, которые имеют дружественный API для разработчиков.

Давайте освежим это в памяти, создав крошечный новостной сайт и соответствующую цепочку функциональных тестов:
composer create-project symfony/skeleton news-website cd news-website/ composer require twig annotations composer require --dev maker tests 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('Join us at SymfonyLive USA 2018!')->link();
$crawler = $client->click($link);
$this->assertSame('Join us at SymfonyLive USA 2018!', $crawler->filter('h1')->text());
}
}
А теперь выполняем тесты:
./bin/phpunit
Все тесты "зеленые"! Symfony предоставляет очень удобный API для навигации по сайту, проверки работоспособности ссылок и для того, чтобы убедиться, что весь ожидаемый контент отображается. Его (API) легко настроить, и он очень быстрый!
При этом, WebTestCase не использует настоящий веб-браузер. Он имитирует его, используя простые компоненты PHP. Он даже не использует HTTP-протокол: WebTestCase создаёт экземпляры объектов Request из HttpFoundation, передаёт их в ядро Symfony и позволяет проверить содержимое экземпляра HttpFoundation Response, возвращаемого приложением. Однако, что, если проблема, препядствующая загрузке веб-страницы, происходит в браузере? Подобные проблемы могут быть самыми разнообразными. Например, ссылка, скрытая неисправным правилом CSS, или "дефолтное" поведение форм, которому препятствует JavaScript-файл с багами, или, что еще хуже, обнаружение браузером уязвимости безопасности в вашем коде.
Что ж, Panther позволяет выполнить этот же сценарий в реальных браузерах! Он также добавляет API BrowserKit и DomCrawler, но под капотом он использует библиотеку Facebook PHP WebDriver. Это означает, что вы можете выбрать: выполнить такой же браузерный сценарий в молниеносно-быстрой чистой PHP-реализации (WebTestCase) или в любом из современных браузеров, с помощью протокола автоматизации браузера WebDriver, которая стала официальной рекомендацией W3C в июне.
Что еще лучше: для того, чтобы использовать Panther, нужна только локальная установка Chrome. Больше ничего не требуется установить: ни Selenium (хоть и Panther также поддерживает его), никаких смутных драйверов или расширений для браузеров... На самом деле, т.к. Panther - это зависимость метапакета symfony/test-pack, вы, не зная об этом, уже установили Panther, когда ранее выполнили composer require --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(); // Так всё ещё работает, если нужно
+ $client = static::createPantherClient();
Еще раз запустим тесты:
./bin/phpunit
Все снова зеленые! Но, на этот раз мы уверены, что наш сайт исправно работает в Google Chrome.
Под капотом Panther:
localhost:9000Если вы верите только тому, что видите, попробуйте запустить следующее:
PANTHER_NO_HEADLESS=1 ./bin/phpunit
Как вы могли заметить в записи, я добавил несколько вызовов к sleep(), чтобы показать, как это работает. Иметь доступ к окну браузера (и к Dev Tools) также очень полезно для исправления проблемного сценария.
Поскольку оба эти инструмента реализуют один и тот же API, Panther может также выполнять веб-парсинговые сценарии, написанные для популярной библиотеки Goutte. В тестовых случаях, Panther позволяет выбрать, должен ли сценарий исполняться с использованием ядра Symfony (по возможности, static::createClient()), используя Goutte (отправить настоящие HTTP-запросы без поддержки Javascript и CSS, static::createGoutteClient()) или используя реальный веб-браузер (static::createPantherClient()).
Даже если Chrome - это выбор по умолчанию, Panther может управлять любым браузером, поддерживающим протокол WebDriver. Он также поддерживает удаленные сервисы тестирования браузера, такие как Selenium Grid (с открытым кодом), SauceLabs и Browserstack.
Существует также экспериментальная ветка, которая использует Geckodriver для автоматического запуска локальной установки Firefox вместо Chrome.
Наш новостной сайт выглядит хорошо, и мы только что доказали, что он работает в Chrome. Но теперь мы хотим получить обратную связь от сообщества по поводу наших частых публикаций. Добавим систему комментариев на сайт.
Для этого мы будем использовать возможности Symfony и современной веб-платформы: мы будем управлять комментариями через веб-API, и будем обрабатывать их с помощью Web Components и Vue.js. Использование JavaScript для этой фичи позволяет улучшить общую производительность и пользовательский опыт: каждый раз, когда мы публикуем новый комментарий, он будет отображаться на существующей странице без полной перезагрузки.
Symfony предоставляет официальную интеграцию с API Platform, вероятно, самый простой путь для создания современных веб API (гипермедиа и/или GraphQL). Установим его:
composer require api
Затем, вновь используем Maker Bundle, чтобы создать класс сущности Comment и сгенерировать endpoint'ы для чтения и записи:
./bin/console make:entity --api-resource Comment
Эта команда является интерактивной, и позволяет указать поля для создания. Нам нужны только два: news (slug новости) и body (содержимое комментария). Поле news имеет тип string (максимальная длина 255 символов), в то время как body имеет тип text. Оба не могут быть null.
Обновим .env файл, чтобы задать адрес вашей РСУБД в DATABASE_URL и выполним следующую команду, чтобы создать таблицу, соответствующую нашей сущности:
./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 - изменения автоматически применятся.
Создание "кастомного" ограничения валидации вместо того, чтобы хардкодить список доступных slug -ов при подтверждении выбора остается в качестве упражнения для читателя.
Это всё по части 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
Обновим config 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>
Это было не так сложно, правда?
Затем, создадим endpoint'ы для нашего приложения комментариев:
// assets/comments/index.js
import Vue from 'vue';
import CommentSystem from './CommentSystem';
new Vue({
el: '#comments',
components: {CommentSystem}
});
Наконец, добавим референс на файл JavaScript и инициализируем веб-компонент <сomment-system> с текущим slug-ом в шаблоне 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
Вау! Благодаря Symfony, мы создали веб-API и классное Vue.js веб-приложение с помощью всего нескольких строк кода. Окей, давайте добавим несколько тестов для нашей системы комментариев!
Стоп... комментарии извлекаются с помощью 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, так и замечательным BrowserKit API для тестирования современных JavaScript-приложений.
Но это еще не все. Т.к. Panther работает с реальными веб-браузерами, он может предоставить те фичи, которые не поддерживаются компонентом BrowserKit: он способен делать скриншоты, чтобы дождаться появления элементов и выполнить кастомный JavaScript в исполняемом контексте страницы. Кроме того, в дополнение к BrowserKit API, 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 - инструменте тестирования для Javascript, основанном на WebDriver, который я использую на протяжении нескольких лет.
Для создания этой библиотеки, я использовал несколько зависимостей, и исправил некоторые проблемы в них:
Open Source - это добродетельная экосистема: вы получаете выгоду от существующих мощных библиотек для создания инструментов высокого уровня, и в то же время вы можете "отдать", улучшая их.
Я также выкладываю низкоуровневые части, которые были созданы в ходе разработки Panther напрямую в PHP WebDriver, а именно:
Эти части затем напрямую помогут всему сообществу, в том числе альтернативам Panther, которые также построены поверх PHP WebDriver (Laravel Dusk, Codeception ...).
Я хочу поблагодарить всех участников этих проектов, и особенно Ondrej Machulda, поддерживающего PHP WebDriver в настоящее время, который уделяет время, чтобы просмотреть и объединить мои патчи. Также отдельное спасибо George S. Baugh, кто добавил поддержку протокола W3C WebDriver в его реализацию Perl WebDriver. Когда я узнал о ней, она мне очень помогла понять чем этот протокол отличается от наследуемого протокола Selenium.
Panther все еще находится в ранней стадии. Чтобы сообщить (или исправить) баг, добавить новую фичу или... поставить ему звезду, переходите по https://github.com/symfony/panther.