Часть 8: DBUnit (Тестирование ПО)

29 May 2017

dbОглавление

Продолжаем серию статей Тестирование ПО, которая посвящена разработке ПО с применением методологии 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 и к реализации становятся обязательны два метода:

В первом на базе значений, которые были заданы в 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(), однако, эта функциональность опциональна.

Литература

Исходный код