Оглавление
Продолжаем серию статей Тестирование ПО, которая посвящена разработке ПО с применением методологии TDD.
В этой части будет рассматривать полезное дополнение к PHPUnit под названием DBUnit. Оно позволяет тестировать базу данных.
На предыдущем занятии мы тестировали форму аутентификации (не путайте процесс аутентификации и авторизации!). Для того чтобы проверить кейс входа существующего пользователя нам надо было создать запись об этом пользователе в бд. Делали мы это в методах. которые выполняются перез запуском тестов.
Писать подобные вещи вручную можно, но представьте, что мы тестируем множество кейсов со множеством пользователей. Каждый раз надо создавать новую запись в бд (и это должны быть не рандомные, а фиксированные записи). Процесс долгий и муторный, а файл с кейсами постепенно разрастается и заполняется данными, которые мы хотим писать в таблички.
Чтобы избежать подобного усложнения тестов и был разработан модуль DBUnit. По сути это просто удобная обвязка, которая берет на себя промежуточную работу по наполнению базы зчначениями.
Установка и настройка
Проще всего установить DBUnit через менеджер пакетов composer. Для этого в каталоге /var/www/ виртуальной машины вводим команду добавления пакета.
composer require --dev phpunit/dbunit ^2
Этот код добавит последнюю версию пакета в раздел require-dev файла composer,json. Затем поставить его и обновит composer.lock. Конечно же вам потребуется закоммитить измененные файлы в систему контроля версий.
Почему мы используем версию 2.x.x, а не 3.x.x? Потому что некоторые модули из шаблона advanced-template еще не адаптированы под новую версию пакета на момент написания этого текста.
Теперь доработаем конфигурацию PHPUnit (файл environments/dev/phpunit.xml) добавив в него строки, которые содержат конфигурацию подключения к бд.
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.7/phpunit.xsd" bootstrap="common/tests/_bootstrap.php"> <!-- старый код оставляем без изменений --> <php> <var name="DB_DSN" value="mysql:dbname=tdd_tests;host=127.0.0.1" /> <var name="DB_USER" value="tdd" /> <var name="DB_PASSWD" value="tdd" /> <var name="DB_DBNAME" value="tdd_tests" /> </php> </phpunit>
Мы добавили секцию php содержащую переменные доступные при запуске тестов в супермассиве $GLOBALS. DBUnit не использует конфигурацию yii. Можно задействовать ее в процессе работы и это будет правильно. Но в общем случае конфигурацию подключения к базе для тестов хранится отдельно. Чуть позже мы увидим как объединить конфигурацию Yii и DBUnit.
Не забываем, что после изменения шаблонов конфигурации нужно выполнить провизию машины. Или руками скопировать файлы в нужное место.
Тестовая база
Предварительно потребуется сформировать набор данных, который будет составлять основу для наших тестов.
Форматов хранения существует несколько. Используем самый простой из них — flatXML.
common/tests/_data/database.xml
<?xml version="1.0" ?> <dataset> <user id="1" username="test" email="test@test.test" auth_key="ssssssssssssssssssssssssssssssss" password_hash="$2y$13$PP1EDCr7ujdhTxZT2DV96uM8e2rcdXHY1xAQINCIiB0gOck/VBwN6" /> <user id="2" username="test1" email="test1@test.test" auth_key="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" password_hash="$2y$13$PP1EDCr7ujdhTxZT2DV96uM8e2rcdXHY1xAQINCIiB0gOck/VBwN6" /> </dataset>
Элементы набора dataset представляют из себя записи, где имя тега — это имя таблицы, куда будет записаны значения, а атрибуты — это значения текущей записи. Не забывайте, что пароли в базе хранятся в хешированом виде, поэтому потребуется каким-либо образом получить хеш пароля из Yii. Итого в таблице user у нас будет две записи — админ и пользователь. Пароли соответственно admin и user (в зашифрованном виде конечно же). Вы можете добавить сюда еще и те записи, которые использовали в своих тестовых сценариях. Это даже стоит сделать для того, чтобы не поломать уже существующие кейсы.
Тестовый сценарий
Редактируем код LoginFormTest и переводим его на использование DBUnit.
common/tests/unit/LoginFormTest.php
namespace common\tests\unit; use common\models\LoginForm; use PHPUnit_Extensions_Database_DataSet_IDataSet; use PHPUnit_Extensions_Database_DB_IDatabaseConnection; use yii\web\User; class LoginFormTest extends \PHPUnit_Extensions_Database_TestCase { protected const USER_EMAIL = 'test@test.test'; protected static $_storedEntities = [ 'user' => null, ]; /** * @var \PDO Подключение к бд. */ protected static $pdo = null; /** * @var \PHPUnit_Extensions_Database_DB_IDatabaseConnection Подключение к базе */ private $_conn = null; protected function getConnection() { if ($this->_conn === null) { if (self::$pdo == null) { self::$pdo = new \PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']); } $this->_conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']); } return $this->_conn; } protected function getDataSet() { return $this->createFlatXMLDataSet( \Yii::$app->getBasePath() . DIRECTORY_SEPARATOR . '../common/tests/_data/database.xml' ); } /** * Add default user to database, * Save original components from engine to temporary storage */ public static function setUpBeforeClass() { foreach (static::$_storedEntities as $entity => $value) { static::$_storedEntities[$entity] = \Yii::$app->get($entity); } } /** * Restore original components after every test */ protected function tearDown() { foreach (static::$_storedEntities as $entity => $value) { \Yii::$app->set($entity, $value); } } // все тесты остаются без изменений }
Первое, что должно бросаться в глаза — это изменение иерархии наследования — теперь наш тест является прямым потомком PHPUnit_Extensions_Database_TestCase и к реализации становятся обязательны два метода:
- getConnection() — получение подключения к тестовой бд
- getDataSet() — загрузка в базу тестового набора данных
В первом на базе значений, которые были заданы в phpunit.xml мы получаем подключение к бд. PHPUnit использует интерфейс \PDO для работы с базой, поэтому не пытайтесь подключаться к базе через mysqli_connect. Второй метод — это источник данных. Посредством хитрых манипуляций внутри самого фреймворка он будет вызван попытке загрузить данные в бд.
Напишем канареечные тесты, которые проверяют, что инфраструктура не содержит ошибок и может использоваться для работы и написания тестов. У нас в базе два пользователя. Поэтому стоит проверить, что оба они существуют и доступны для манипуляций.
public function testTestUserExists() { $user = \common\models\User::findByEmail(self::USER_EMAIL); $this->assertNotEmpty($user); } public function testTest1UserExists() { $user = \common\models\User::findByEmail('test1@test.test'); $this->assertNotEmpty($user); }
У нас уже был testOne — переименовываем его в testTestUserExists.
Запуск тестов должен показать, что все работает и пользователи существуют в базе. Если нет, что есть определенные проблемы с инфраструктурой, которые самое время решить.
Если у вас были свои кейсы и к датасету были добавлены нужные записи, то он заработают без какой-либо доработки. Возьмем для примера тест, который проверяет авторизацию пользователя.
public function testTestUserLogin() { $mock = $this->getMockBuilder(User::class) ->setMethods(['login']) ->disableOriginalConstructor() ->getMock(); $mock->method('login')->withAnyParameters()->willReturn(true); \Yii::$app->set('user', $mock); $loginForm = new LoginForm(); $loginForm->load(['LoginForm' => ['email' => static::USER_EMAIL, 'password' => static::USER_PASSWORD]]); $this->assertTrue($loginForm->login()); }
Чтобы пока не разбираться с авторизацией в консоли подменим метод \yii\web\User::login() своим, который возвращает true. Это необходимо потому что в консоли отсутствуют объекты Request и сессии. К ним мы еще вернемся.
Тест работает. Ошибок пока не находит. Отметим, что работать с базой при помощи DBUnit гораздо приятнее, нежели руками через setUp.
Внутреннее устройство
Для того, чтобы лучше понимать, как происходит тестирование с рассматриваемым фреймворком посмотрим, как это устроено.
Конечно же стоит заметить, что поле id или любой другой первичный ключ (если он не является автогенерируемым должен присутствовать в датасете. И автогенерируемый ключ так же стоит включать в датасет — это обеспечит одинаковое состояние набора данных при запуске теста.
1. Очистка базы
Прежде чем хоть один тест будет запущен PHPUnit выполняет операцию TRUNCATE для всех таблиц, которые указаны в датасете.
2. Загрузка фикстур
PHPUnit проходит по всему набору данных из датасета и выполняет операцию INSERT чтобы вставить строки данных.
3–5. Запуск тестов, проверка и завершение (tearDown)
Как только состояние базы данных обнулено и загружены фикстуры, фреймворк запускает тесты. Никаких действий со стороны разработчика не требуется.
Тест может вызывать метод assertDataSetsEqual(), однако, эта функциональность опциональна.