Янв10
0

Что такое тестирование в PHP и с чем его едят

Yii 2 Share this post

Codeception

PHPUnit сам по себе никуда не делся, однако ни одно приложение не в состоянии выжить только за счет unit-тестов. Со временем появился Codeception, который представляет из себя большую надстройку над PHPUnit, Selenium и еще кучи пакетов, в которые я сам не заглядывал. Codeception предоставляет три вида тестирования:

  • Acceptance — «приемочное» тестирование, проверка работы пользователя с приложением на основе сценариев (BDD).
  • Functional — функциональное тестирование, функциональная проверка работы приложения.
  • Unit — модульное тестирование, низкоуровневое тестирование конкретных функций.

Unit testing

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

Functional testing

Функциональное тестирование появилось тогда, когда стало ясно, что кроме проверки функций необходимо проверять именно работу приложения — что оно не выдает 404 для существующих данных, выдает 403 для закрытых секций, выдает 400 на неправильно введенных данных. Функциональное тестирование постепенно развивалось — коды ответа кодами ответа, а разработчиков интересует именно получение пользователем той или иной информации на странице. Здесь и появилась такая штука, как selenium — сервер, который позволяет запускать браузер и взаимодействовать с ним, и webspider — эмуляция браузера, которая позволяет так же «ходить по страницам».

Acceptance testing

Codeception же несколько переформатировал понятие функционального тестирования и ввел acceptance testing. В codeception функциональное тестирование — это тестирование страниц через веб-спайдера, acceptance testing — тестирование страниц через браузер. Формально эти тесты могут совпадать с точностью до названия класса, но acceptance testing способен проверять реальную видимость html-элементов на странице и отслеживать работу javascript.

Yii первой версии существовал в отрыве от общих стандартов в пользу удобства использования (хотя, честно говоря, не знаю, были ли тогда стандарты). Yii 2 пришел и к неймспейсам, и к обильному использованию чужих пакетов, в том числе codeception. Codeception ставится так же, как и все остальные зависимости Yii — через composer

Как использовать codeception?

Codeception, как и большинство современных пакетов, предлагает работать с ним через командную строку. Основной исполняемый файл — app_dir/vendor/codeception/codeception/codecept, для удобства я буду писать просто codecept. Проще всего установить codeception глобально и настроить команду-алиас на соответсвующий файл.

Сначала необходимо проинциализировать приложение:
codecept bootstrap
Это создаст папку tests и создаст внутри всю необходимую инфраструктуру. В примерах Yii это уже будет сделано.
После этого необходимо настроить codeception с помощью YAML-файлов в tests, но это лучше смотреть сразу в документации.

Codeception предоставляет возможность писать тесты как на основе PHPUnit_Framework_TestCase (CTestCase из Yii 1.х был оберткой вокруг него), так и на основе своей обертки \Codeception\TestCase\Test. Создать тест проще всего опять же из консоли (предположим, что тест создается для модуля HttpRequest)
codecept generate:test unit HttpRequest
// Test was created in %app.root%/tests/unit/HttpRequestTest.php

Codeception сам добавляет суффикс Test (который позволяет отделять классы-тесты) и расширение .php. Так как скорее всего тестов будет много, можно сразу указать папку, где должен валяться тест. Предположим, полное название HttpRequest на самом деле \Core\Http\HttpRequest и валяется он в папке %app.root%/Core/Http, тогда будет разумно поместить тест в папку %app.root%/tests/Core/Http:
codecept generate:test unit Core/Http/HttpRequest
// Test was created in %app.root%/tests/unit/Core/Http/HttpRequestTest.php

Прекрасно, а что мне теперь писать в этом файле?

Внутри файл находится класс теста. Этот класс представляет из себя набор методов-тестов, которые проверяют поведение кода, и дополнительных методов инфраструктуры.
Каждый метод-тест начинается с префикса test (наприме, testValidate; хорошим тоном будет подставлять название тестируемого метода после префикса, если в тесте проверяется только один метод). PHPUnit посканирует класс и обнаружит все методы, начинающиеся с этого префикса, а затем будет запускать один за другим. Каждый метод теста проверяет поведение кода, используя assert-методы PHPUnit, каждый такой метод проверяет полученные данные. Например, $this->assertTrue($httpRequest->isPostRequest); проверит, соответствует ли true значение $httpRequest->isPostRequest, и в случае, если проверка провалится, оборвет выполнение теста (т.е. только одного метода, если не используется @depends).
По поводу «инфраструктурных» методов: любое тестирование сталкивается с тем, что для тестирования требуется настроить окружение, загрузить какие-то данные, создать фальшивые объекты, создать массивы однотипных данных для проверки. Самыми главными здесь будут _before() и _after() (setUp() / tearDown() в PHPUnit) — они будут выполняться до и после выполнения каждого теста в классе, в них обычно выполняет подготовку фальшивого окружения/объектов.

Пример класса-теста:
<?php
use Codeception\Util\Stub;
use \Core\Http\HttpRequest;

class HttpRequestTest extends \Codeception\TestCase\Test
{
protected $codeGuy;
public static function setUpBeforeClass()
{
$_SERVER
['REQUEST_METHOD'] = 'POST'; // в консоли, вероятнее всего, этого ключа изначально вообще не будет
}
public static function tearDownAfterClass()
{
unlink
('dummy.html'); // стираемый созданный тестом файл
}
public function testIsPostRequest()
{
$request
= new HttpRequest; // создаем тестируемый объект. В зависимости от характера теста, создание, возможно, будет разумнее вынести в _before()
$this
->assertTrue($request->isPostRequest); // выполняем непосредственно проверку поведения, сравнивая isPostRequest с true
}
}

А как запустить написанное?

Как всегда — из командной строки. Следующая команда запустит все тесты:
codecept run
Т.к. часто придется обращаться к отдельному виду тестирования или вообще тесту (если отвалился единственный модуль, то толку-то все гонять?), то лучше запускать тесты по этому виду или вообще конкретный тест:
codecept run unit // все юнит-тесты
codecept run tests
/unit/Core/Http/HttpRequestTest.php // конкретный тест, требуется относительный путь с суффиксом и расширением.

После этого codeception выведет количество тестов, проверок (один тест может включать в себя несколько проверок) и ошибок. Понятное дело, что любое количество ошибок, отличное от нуля, должно восприниматься как build: failed, и соответствующий код должен немедленно исправляться.

Эй, ты ничего не рассказал про остальные два вида тестирования!

Потому что сам в них толком пока не поварился (а еще мне лень). В принципе, базовый getting started и любая IDE с подсветкой подскажут что к чему — они ОЧЕНЬ интуитивны.


(предыдущая версия текста про PHPUnit)

Модульные тесты (unit tests) в PHP в любом фреймворке всегда упираются в один и тот же пакет: phpunit. С чем бы вы не столкнулись, дело придется иметь именно с ним, возможно, в небольшой обертке.
Сама суть модульного тестирования заключается примерно в следующем: при разработке какого-то приложения после появления (или изменения) запланированного функционала этого приложения параллельно с самим приложением пишется тестовое сопровождение. Тестовое сопровождение — это, как правило, набор классов, которые по наименованию и расположению повторяют тестируемое приложение, например:
Приложение:
models/ProductModel.php
components/CrmService.php

Тесты:
tests/models/ProductModelTest.php
tests/components/CrmServiceTest.php

(контроллеры тут пока специально не упомянуты)
Каждый класс — это набор тестов для соответствующего компонента, реально этот набор представлен методами внутри класса теста для этого компонента. PHPUnit автоматом запускает все методы класса, которые начинаются с test (и некоторые другие, но об этом в документации), поэтому типичный класс теста будет представлять собой примерно такую картину:
class ProductModelTest extends TestCase { // в данном случае TestCase - та самая обертка, которая будет наследоваться от PHPUnit_Framework_TestCase
public function testGet() // негласная конвенция - называть метод по имени тестируемого метода
{...} // проверка всевозможных комбинаций входных и выходных значений для метода ProductModel::get()
public function testGetAll()
{...}
public function testValidation()
{...}
}

Теперь непосредственно к yii2. Само приложение advanced представляет из себя просто скелет (точнее, я нашел в common один тест, но он не смог найти codeception), поэтому нам сначала надо будет написать какое-нибудь подобие модели, например, такое. В этой псевдомодели нужно оттестировать все методы, чтобы а) не забыть о недописанных в будущем б) быть уверенным, что в любых стрессовых ситуациях система поведет себя именно так, как надо и в) быть оповещенным ровно в тот момент, как что-то отвалится из-за переписывания низкоуровневых компонент. Соответственно, каждый метод (благо их здесь всего ничего) нужно нашпиговать всеми возможными приходящими значениями и убедиться в корректности отработки (точнее, шпиговать его каждый раз, как что-то поменялось). Я написал для этой модели небольшой и дурацкий тест (ссылка), который занимается ровно этим. Теперь можно проверить работоспособность всей этой конструкции, если модель закинуть в common/models/ProductModel.php, а тест в common/tests/unit/models/ProductModelTest.php. После этого в директории common/tests надо будет выполнить следующую комманду:
phpunit --bootstrap _bootstrap.php unit/models/ProductModelTest.php
Это запустит соответствующий тест (по умолчанию phpunit будет пытаться загрузить все файлы с названием *Test.php из текущей папки и подпапок; подробнее, как всегда, в доках) и даст примерно следующий вывод:
PHPUnit 3.6.10 by Sebastian Bergmann.

.......

Time: 0 seconds, Memory: 3.00Mb

OK (7 tests, 18 assertions)
Который говорит о том, что 7 тестов прошли успешно. Когда я писал модель, я в проверке на id вместо < 1 указал < 0, и в результате все невалидные айдишники прошли, о чем меня и уведомила тестовая проверка:
PHPUnit 3.6.10 by Sebastian Bergmann.

F......

Time: 0 seconds, Memory: 3.00Mb

There was 1 failure:

1) common\tests\unit\models\ProductModelTest::testGet Exception was expected for string input

/srv/sandbox/yii2/apps/advanced/common/tests/unit/models/ProductModelTest.php:14

FAILURES! Tests: 7, Assertions: 6, Failures: 1.
Такой вот сумбурный ответ.
Что касается остальных папок — это файлы для поддержки codeception (в папках с подчерком в начале названия), с которым я сам пока не разобрался (это тестирование более высокого уровня, включающее в себя phpunit), и файлы для различных видов тестирования. На самом деле PHPUnit годится только для библиотек, а для приложений является полумерой, пусть и довольно хорошо защищающей — в приложении должны работать не только модели, но и контроллеры, а для этого недостаточно сымитировать ситуацию — нужно поднять nginx/apache (а лучше оба) и убедиться, что на конкретные запросы приходят верные ответы. Это называется функциональным тестированием, до чего я сам пока не дошел, и осуществляется с помощью загадочной для меня (пока что) программы selenium. Папка functional, соответственно, отвечает за функциональное тестирование. acceptance, насколько понял — это папка для codeception-тестов, который идет еще дальше, и вместо простой проверки запросов/ответов эмулирует клики и отображение для пользователя. Все вместе это называется Continuous Integration (CI) и обеспечивает невозможность выложить в продакшен с некислой посещаемостью и функционалом какой-нибудь серьезный ляп. Я по выходным все пытаюсь развернуть себе полноценный сервер с поддержкой всего этого веселья, но реально в проектах мне пока хватало PHPUnit — остальное, конечно, полезно, но проекты не того масштаба )
В любом случае для ознакомления с тестированием придется сначала поработать только с PHPUnit, поэтому об остальном можно пока не задумываться.

About the Author

Leave a Reply