Ловили себя когда-нибудь на мысли, что изменение в одном месте кода приводит к ошибке в совершенно другом? Такие ошибки называются регрессионными. А теперь представьте, что вы пишите API для крупного сервиса. Как часто нужно проверять, что все компоненты работают, аутентификация не отваливается, а данные выдаются именно в том виде, в котором необходимо?
Чтобы избежать регрессионных ошибок и постоянного ручного повторения рутинных тестов, существуют тесты автоматизированные.
В этой статье вы узнаете, как подключить тесты к REST API сервису на языке PHP. Мы будем использовать пакет PHPUnit для тестирования и микро-фреймворк Slim для создания протопипа веб-приложения. Однако, шаги применимы к проектам на любых фреймворках и языках.
Установка
Не будем подробно останавливаться на установке и настройке Slim framework. Приведём краткую инструкцию, как создать простой Hello-World-API и настроить автоматическое тестирование.
В нашем примере будут использоваться менеджер пакетов для PHP – composer и база данных mongodb. Ознакомиться подробнее с их настройкой вы можете в статье по ссылке.
Ставим Slim 3
mkdir codex.unit.tests
cd codex.unit.tests
composer require slim/slim "^3.0"
Создайте файл index.php со следующим содержимым
<?php
require 'vendor/autoload.php';
$app = new \Slim\App(['settings' => [
'displayErrorDetails' => true,
]]);
$app->get('/', function ($request, $response, $args) {
return 'Hello, world!';
});
include 'userModel.php';
include 'routes.php';
$app->run();
Создаём простой API
Определим модель пользователя в файле userModel.php
<?php
class userModel
{
public $mongo;
public function __construct($username='')
{
$this->mongo = new MongoDB\Driver\Manager("mongodb://db:27017");
}
public function find($param, $value)
{
$query = new MongoDB\Driver\Query([$param => $value]);
$cursor = $this->mongo->executeQuery('local.users', $query);
$result = $cursor->toArray();
return $result;
}
public function get($id)
{
$query = new MongoDB\Driver\Query(['id' => $id]);
$cursor = $this->mongo->executeQuery('local.users', $query);
$result = $cursor->toArray();
if (!$result) {
return false;
}
return ['id' => $result[0]->id, 'username' => $result[0]->username, 'password' => $result[0]->password];
}
public function create($username, $password)
{
$id = (string)new MongoDB\BSON\ObjectId();
$bulk = new MongoDB\Driver\BulkWrite;
$bulk->insert(['id' => $id, 'username' => $username, 'password' => $password]);
$result = $this->mongo->executeBulkWrite('local.users', $bulk);
if ($result) {
return (string)$id;
} else {
return false;
}
}
}
Добавим несколько маршрутов в файле routes.php
<?php
$app->post('/api/user', function ($request, $response, $args) {
$params = $request->getParsedBody();
if (!isset($params['username']) || !isset($params['password'])) {
return json_encode(['result' => 'error', 'body' => 'invalid input']);
}
$user = new userModel();
if ($user->find('username', $params['username'])) {
return json_encode(['result' => 'error', 'body' => 'user exists']);
}
$result = $user->create($params['username'], $params['password']);
if ($result) {
return json_encode(['result' => 'success', 'body' => ['id' => $result]]);
}
return json_encode(['result' => 'error', 'body' => 'insert failed']);
});
$app->get('/api/user/{user_id}', function ($request, $response, $args) {
$user = new userModel();
$result = $user->get($args['user_id']);
if ($result) {
return json_encode(['result' => 'success', 'body' => $result]);
}
return json_encode(['result' => 'error', 'body' => 'user not found']);
});
API доступен по адресу http://localhost/api/
Сейчас он позволяет выполнять следующие действия:
- Создание нового пользователя
- Вывод списка пользователей
- Вывод информации о конкретном пользователе по ID
- Удаление пользователя по ID
Сценарии тестирования
При создании нового метода или компонента системы вам придётся тестировать работоспособность остальных частей API. Рассмотрим один из сценариев тестирования:
- Подключиться к тестовой базе данных, предварительно очистив её.
- Создать нового пользователя
curl -X --data "username=firstUser&password=pwd" http://localhost/api/user/
- Проверить, что сервер вернул ответ с кодом 200 и следующим содержимым
{'result': 'success', 'body': {'id': '5a739ea9c791d20006068246'}}
- Получить информацию о пользователе с полученным id
curl http://localhost/api/user/5a739ea9c791d20006068246
- Проверить, что сервер вернул результат с содержимым
{'result': 'success', 'body': {'id': '5a739ea9c791d20006068246', 'username': 'firstUser', 'password': 'pwd'}}
- Получить список пользователей (реализация данной функции предлагается читателю как самостоятельное задание)
curl http://localhost/api/users
- Проверить, что сервер вернул результат с содержимым
{'result': 'success', 'body': {'count': 1, 'ids': ['5a739ea9c791d20006068246']}
- Проверить, что сервер выдаёт ошибку при попытке получить несуществующего пользователя
curl http://localhost/api/user/1337
...
{'result': 'error', 'body': 'user not found'}
А теперь было бы неплохо ещё раз очистить базу и попробовать сгенерировать несколько пользователей, а также проверить данные методы на них.
Представьте, что эти действия нужно выполнять постоянно. А ведь мы написали пока только hello-world API. В настоящем проекте будет гораздо больше данных с более сложными взаимосвязями.
Автоматизированное тестирование позволяет упростить эту задачу.
Подключаем PHPUnit
Откройте файл composer.json и допишите в него строчку
"require-dev": {
"phpunit/phpunit": "5.4.*"
}
Либо выполните команду
composer require-dev "phpunit/phpunit=5.4.*"
В результате должно получиться примерно следующее содержимое
{
"name": "codex.unit.tests",
"authors": [
{
"name": "Nostr and CodeX Team",
"email": "[email protected]"
}
],
"require": {
"slim/slim": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "5.4.*"
}
}
Не забудьте обновить зависимости composer
composer update
Создадим директорию для будущих тестов
mkdir tests
В директории сохраним первый файл DummyTest.php для тестирования
<?php
class DummyTest extends PHPUnit_Framework_TestCase
{
public function testAlwaysTrue()
{
$this->assertTrue(true);
}
}
Для запуска выполните следующую команду из корня проекта
./vendor/bin/phpunit ./tests/DummyTest.php
Тест всегда будет пройден успешно.
PHPUnit 5.4.8 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 30 ms, Memory: 2.25MB
OK (1 test, 1 assertion)
Автоматизируем тесты
Создадим отдельный файл UsersTest.php
<?php
require 'vendor/autoload.php';
include 'userModel.php';
class UsersTest extends PHPUnit_Framework_TestCase
{
public function testModelUnexistingUser()
{
$user = new userModel();
$this->assertFalse($user->get("123"));
}
}
Запустите проверку тестов из корневой директории проекта с помощью команды
vendor/bin/phpunit tests/UsersTest.php
Вы увидите сообщение об успешном прохождении тестов как в примере выше.
PHPUnit предлагает несколько функций, упрощающих сравнение тестовых данных с эталонными. В функции testModelUnexistingUser мы сравнивали результат выполнения выражения $user->get("123") с False. Делали мы это с помощью метода assertFalse, который выдаст ошибку в случае, если результат не будет равен False.
Существуют и другие методы, например:
- assertTrue – сравнение с True
- assertStringMatchesFormat – проверка соответствия строки заданному формату
- assertNotEquals – проверка, что два аргумента функции не равны друг другу
- assertEquals – проверка на равенство аргументов
- assertArrayHasKey – убеждаемся, что в массиве существует ключ с заданным значением
С полным перечнем вы можете ознакомиться по ссылке.
Допишем тесты модулей
<?php
require 'vendor/autoload.php';
include 'userModel.php';
class UsersTest extends PHPUnit_Framework_TestCase
{
public function testModelUnexistingUser()
{
$user = new userModel();
// запрос несуществующего пользователя возвращает False
$this->assertFalse($user->get("123"));
}
public function testModelCreateNewUser()
{
$user = new userModel();
$result = $user->create('testUsername', 'testPassword');
// результат запроса на создание пользователя возвращает Hex строку
$this->assertStringMatchesFormat("%x", $result);
}
public function testModelExistingUser()
{
$user = new userModel();
$id = $user->create('testUsername', 'testPassword');
$result = $user->get($id);
// создаём пользователя и проверяем результат запроса по его ID
$this->assertNotEquals(false, $result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($id, $result['id']);
}
}
Остальные тесты читателю предлагается реализовать самостоятельно по аналогии.
Настройки перед тестами
В текущей реализации, база данных не очищается перед началом тестирования. Можно добавить в класс UsersTest специальную функцию setUp. PhpUnit будет выполнять её перед тем, как запустить каждый из тестов.
Добавляем профиль тестирования
Все настройки тестирования можно вынести отдельно. Создайте файл phpunit.xml в корне проекта и запишите в него следующее содержимое:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
Настройки описывают phpunit директорию поиска тестов (папка tests), названия файлов с тестами (имя оканчивается на "Test.php") и файл с настройками тестирования (tests/bootstrap.php).
Тесты теперь можно запускать командой без параметров
vendor/bin/phpunit
В файл bootstrap.php можно вынести настройки, которые будут добавляться перед каждым запуском. Например, можно вынести следующие строчки из файлов с тестами и перенести их в bootstrap.php.
require 'vendor/autoload.php';
include 'userModel.php';
Подробнее о возможностях конфигурирования PHPUnit можно прочитать в официальной документации.
Добавляем команду для Composer
Консольные команды удобно хранить в одном месте и вызывать единообразно. Обычно, роль такого интерфейса играет Composer. Чтобы запускать проверку тестов из Composer, нужно добавить секцию scripts в файл composer.json
{
"name": "codex.unit.tests",
"authors": [
{
"name": "Nostr and CodeX Team",
"email": "[email protected]"
}
],
"require": {
"slim/slim": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "5.4.*"
},
"scripts": {
"test": "vendor/bin/phpunit"
}
}
После данной манипуляции запускать тесты можно командой:
composer test
Теперь вы знаете, как улучшить надёжность вашего кода при помощи автоматического тестирования. Умение писать тесты для своего кода сегодня является необходимым практически для любого разработчика начиная с уровня middle, а иногда и junior. И не забывайте почаще заглядывать в папку tests у ваших любимых репозиториев на GitHub.
Ссылки
- Модульное тестирование (терминология)
- Проект нашей команды с использованием Slim PHP и Unit tests