Создание autoselect-компонента на Symfony/Vue. Часть 3. Тесты.

Сейчас на новой работе я вникаю в огромный флоу поверх разработки. Тут нельзя просто запушить в мастер, тут нужно пройти огромную череду этапов: начиная от правильного наименования коммита и тестов (как самописных, так и уже существующих), заканчивая получением одобрений от старших коллег (а это сделать, временами, сложно 🙂 ). И только после этого влить свою ветку в мастер и радоваться успешной правке.

И на фоне этого я понял, насколько важно приучаться себя ещё на моменте обучения разрабатывать правильно. Не мне говорить о правильной разработке — этому я только учусь, но зато сказать что тесты в ней точно участвуют — уже могу.

Сегодня мы обложим тестами тот компонент, который закончили писать на предыдущем уроке. В этой части затронем бэк, в следующей — фронт.

Кстати, компонент (и back,и front-части) я залил на битбакет, чтобы ты сразу мог посмотреть код. Ссылки — фронт, бэк, и в конце статьи.

Для начала смотрим в ман от Symfony про тестирование — https://symfony.com/doc/current/testing.html.

Нам говорят, что чтобы начать писать тесты, нам нужен компонент, который реализует логику для работы с тестами — symfony/phpunit-bridge, являющийся обвязкой над PHPUnit — фреймворком для тестирования PHP. Ставим:

 

composer require --dev symfony/phpunit-bridge

В Symfony-приложении все тесты находятся в директории /tests. Насколько я понимаю — классы, которые ты тестируешь, можно закидывать туда в любом порядке, но крайне рекомендуют повторять структуру и иерархию самого проекта. Грубо говоря, у нас в /src есть директория Controller и, скажем, Command. Тогда в папке /tests будут так же папки Controller и Command. Да, к слову — все классы, в которых описаны тесты, по соглашению должны заканчиваться на ‘Test’, а все методы, описанные в классах, должны начинаться со слова test.

Сначала следует сказать, что есть несколько видов тестов. На самом деле их очень много, но мы, в контексте нашего обучения, будем рассматривать два самых важных и постоянно используемых вида тестов — функциональные тесты и unit-тесты.

Unit-тесты — это тесты на какой-то класс, который выступает отдельной единицей в нашем приложении. Хороший и простой пример приведён в документации — класс, реализующий логику работы калькулятора. Калькулятор — это сущность, которая является полноценным модулем, где нужно тестировать весь публичный интерфейс, и делается это как раз с помощью Unit-тестов. Нагло скопирую пример из документации для наглядности:

// src/Util/Calculator.php
namespace App\Util;

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

Тесты на него выглядят следующим образом:

// tests/Util/CalculatorTest.php
namespace App\Tests\Util;

use App\Util\Calculator;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function testAdd()
    {
        $calculator = new Calculator();
        $result = $calculator->add(30, 12);

        // assert that your calculator added the numbers correctly!
        $this->assertEquals(42, $result);
    }
}

Создаётся класс CalculatorTest, который наследуется от класса TestCase — класса, реализующего интерфейс для тестирования с помощью фреймворка PhpUnit. В классе CalculatorTest тестами обкладываются все методы, входящие в публичный интерфейс класса. В приведённом выше примере такой метод один — add. Нам важно, чтобы метод add складывал два числа и возвращал результат их сложения, что мы и должны протестировать. В примере создаётся объект Calculator, вызывается метод add, а затем нам нужно проверить, действительно ли метод add сложил 2 числа и получил верный результат. Мы можем утверждать, что 30 + 12 = 42. Поэтому мы можем вызвать так называемое «утверждение» или assert — метод, который что-то утверждает (например, утверждает, что два числа равны). Assert-методов много, все они используются для утверждения чего-то, почитать про них можно тут, В примере выше используется assertEquals, так как именно этот тип утверждения сравнивает 2 переменных. Тест выполнится успешно, если assertEquals вернёт true. Если false — тест упадёт. Сама суть Unit-тестов — тестировать отдельные модули приложения посредством различных утверждений.

А зачем тогда нужны функциональные тесты? Этот тип тестов нужен, чтобы тестировать функционал приложения. В Symfony-приложениях функциональными тестами обкладывают, как правило, контроллеры, потому что это и является основным функционалом приложения. Функциональные тесты так же могут вызывать утверждения, но помимо этого они могут анализировать html/xml-код, который контроллер отдаёт клиенту, отправлять формы, переходить по ссылкам и ещё много полезных вещей, которые мы будем рассматривать в дальнейшем. Так как в нашем приложении всего один контроллер и нет сервисов, сегодня мы будем работать именно с этим видом тестов.

У нас один контроллер, который возвращает массив продуктов по переданному фильтру:

/**
 * @Route("/product", name="product", methods="GET")
 */
public function getProducts(Request $request, ProductRepository $productRepository)
{
    $filter = $request->query->get('filter');
    $products = $productRepository->findByFilter($filter);

    return $this->json([
        'products' => $products,
    ]);
}

Что мы можем тут протестировать? Например то, что переданный get-параметр соответствует полученному. Так же то, что $products — это массив, и по переданному фильтру мы можем утверждать, что этот массив не пустой. Так как мы отдаём json-ответ, анализировать html-код нам не требуется, но в будущем мы обязательно это попробуем. А пока смотрим в доку, где показано, как тестировать контроллеры — линк. Первым делом ставим зависимости, необходимые для функциональных тестов:

composer require --dev symfony/browser-kit symfony/css-selector

После чего создаём директорию tests/Controller, и в ней создаём класс ProductControllerTest и определяем в нём метод testGetProducts:

<?php

namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ProductControllerTest extends WebTestCase
{
    public function testGetProducts()
    {
        
    }
}

Мы наследуем наш класс от WebTestCase. Для Unit-тестирования в примере выше мы наследовались от класса TestCase. В чем разница? Класс WebTestCase дополняет класс TestCase описанными выше возможностями — переходить по ссылкам, отправлять формы и так далее.

В методе, как и показано в документации, первым делом создаём клиент, который будет имитировать клиентский браузер:

$client = static::createClient();

Что делает клиент? Как правило, посылает запрос. Поэтому следующей строкой мы отправим запрос на наш контроллер, который мы тестируем:

$client->request('GET', '/product', ['filter' => 'w']);

1-ый параметр — метод, второй — относительная ссылка, третий — параметры запроса. Если мы не передадим третий параметр и запустим тесты, получим ошибку:

Запрос улетел в контроллер и сломался на этапе передачи аргумента в метод findByFilter, так как аргумент у нас типизированный. Теперь вернём 3-ий аргумент в создание request’а и запустим тесты ещё раз:

Теперь нам нужно проверить, что ответ, полученный от сервера — 200-ый. Для этого используем уже знакомое нам утверждение assertEquals:

 

$this->assertEquals(200, $client->getResponse()->getStatusCode());

Чтобы получить код ответа, из объекта $client мы можем извлечь объект getResonse(), а уже из него получить код. Нам осталось проверить, что данные, полученные по переданному фильтру, соответствуют тем, которым мы ожидаем получить. Вспомним, что лежит у нас в базе:

И сделаем запрос с переданным фильтром из интерфейса:

Соответственно, мы можем утверждать, что $products содержит 4 строки, которым мы можем явно перечислить. Обратимся к документации и посмотрим, как это проверить. Документация говорит, что есть утверждение assertArraySubset, которое проверяет, есть ли в массиве какой-либо подмассив. Я залогировал то, в каком формате приходит нам ответ и получилось вот такое правило:

$this->assertArraySubset(['products' => [
    0 => [
        'id' => 5,
        'name' => 'Gerlach, Lueilwitz and Hoeger',
        'add_date' => [
            'date' => '1975-06-26 18:05:01.000000',
            'timezone_type' => 3,
            'timezone' => 'Asia/Krasnoyarsk'
        ],
        'count' => 60
    ]
]
],
    json_decode($client->getResponse()->getContent(), true));

Да, выглядит страшно, но мы точно утверждаем, что в полученном ответе есть подмассив с указанными данными. Запускаем тесты и смотрим:

На этом сегодня мы закончим. Мы разобрались, в чём отличие unit-тестов от функциональных тестов, написали тест и поразбирались с утверждениями. Весь код доступен по ссылкам ниже — можешь ознакомиться 🙂 Всегда рад комментариям — на всё отвечу и помогу, если будут какие-то вопросы. Через пару дней будет статья по тестированию front-части. До встречи 🙂

Ссылки:

Фронт-часть autoselect-компонента — https://bitbucket.org/junsenior/vue-autoselect-front/src/master/

Бэк-часть autoselect-компонента — https://bitbucket.org/junsenior/symfony-autoselect-back/src/master/

Первая часть статьи —  Создание autoselect-компонента на Symfony/Vue. Часть 1. или https://telegra.ph/Sozdanie-autoselect-komponenta-na-SymfonyVue-05-19

Вторая часть статьи — Создание autoselect-компонента на Symfony/Vue. Часть 2. или https://telegra.ph/Sozdanie-autoselect-komponenta-na-SymfonyVue-CHast-2-06-11

Twitter — https://twitter.com/SeniorJun — анонсы, мысли и обсуждения. Тут обычно то, что в канал я не рискну писать 🙂

Иии в качестве эксперимента — https://www.patreon.com/junsenior — можно поддержать проект и посты будут чаще и лучше 🙂

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *