Автоматизация тестирования на PHP

12 min read

Ловили себя когда-нибудь на мысли, что изменение в одном месте кода приводит к ошибке в совершенно другом? Такие ошибки называются регрессионными. А теперь представьте, что вы пишите 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/

Сейчас он позволяет выполнять следующие действия:

  1. Создание нового пользователя
  2. Вывод списка пользователей
  3. Вывод информации о конкретном пользователе по ID
  4. Удаление пользователя по ID 

Сценарии тестирования

При создании нового метода или компонента системы вам придётся тестировать работоспособность остальных частей API. Рассмотрим один из сценариев тестирования:

curl -X --data "username=firstUser&password=pwd" http://localhost/api/user/
{'result': 'success', 'body': {'id': '5a739ea9c791d20006068246'}}
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": "team@ifmo.su" } ], "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.

Существуют и другие методы, например:

С полным перечнем вы можете ознакомиться по ссылке.

Допишем тесты модулей

<?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": "team@ifmo.su" } ], "require": { "slim/slim": "^3.0" }, "require-dev": { "phpunit/phpunit": "5.4.*" }, "scripts": { "test": "vendor/bin/phpunit" } }

После данной манипуляции запускать тесты можно командой:

composer test

Теперь вы знаете, как улучшить надёжность вашего кода при помощи автоматического тестирования. Умение писать тесты для своего кода сегодня является необходимым практически для любого разработчика начиная с уровня middle, а иногда и junior. И не забывайте почаще заглядывать в папку tests у ваших любимых репозиториев на GitHub.

Ссылки

  1. Модульное тестирование (терминология)
  2. Проект нашей команды с использованием Slim PHP и Unit tests