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

Оглавление

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 и к реализации становятся обязательны два метода:

  • 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(), однако, эта функциональность опциональна.

Литература

Исходный код

Категории: Разработка HowTo