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