PHP Profi

Symfony Panther: PHP-библиотека для тестирования из браузера и парсинга веб-страниц Перевод

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

Помощник WebTestCase

Давайте освежим это в памяти, создав крошечный новостной сайт и соответствующую цепочку функциональных тестов:

 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) легко настроить, и он очень быстрый!

Используем Panther для запуска сценария в браузере

При этом, 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:

  • запустил ваш проект с помощью встроенного в PHP веб-сервера по localhost:9000
  • запустил версию Chromedriver, которая поставляется с библиотекой для автоматизации локального Chrome
  • выполнил браузерный сценарий, определенный в тесте с Chrome в режиме headless

Если вы верите только тому, что видите, попробуйте запустить следующее:

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.

Тестирование HTML, сгенерированного на стороне клиента

Наш новостной сайт выглядит хорошо, и мы только что доказали, что он работает в 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-приложений.

Дополнительные возможности (скриншоты, внедрение JS)

Но это еще не все. Т.к. 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.

Спасибо, Open Source

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.

2020-11-12 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий