
Основная задача, которую решает данное расширение, это автоматическое и удобное приведение базы данных в требуемое состояние перед каждым тестом. Т.е. наполнение таблиц нужными данными (фикстурами).
Установка.
В зависимости от используемой версии PHP, немного отличается процесс установки и подключения расширения DBUnit. Разумеется предварительно у вас должен уже быть установлен PHPUnit, т.к. при тестировании работы с БД используются все те же принципы и методы.
Чтобы поддерживался php версий 5.4 - 5.6 (а так же 7.0, 7.1) нужно установить DBUnit версии 5.7. Глобальная установка с помощью Composer (версия DBUnit 5.7):
composer global require --dev phpunit/dbunit ^2В данном случае расширение DBUnit установится глобально в папку текущего пользователя, например
C:\Users\USER_NAME\AppData\Roaming\Composer\vendor\phpunit
Обычно phpunit устанавливают глобально, чтобы была возможность протестировать любой проект и не устанавливать для каждого отдельно. Если у вас так, то и DBUnit устанавливаем соответственно.
Для не глобальной установки (в папку vendor проекта) удалить параметр «global».
Если поддержка php 5-й версии не нужна, то устанавливаем так:
composer global require --dev phpunit/dbunitчто установит последнюю версию расширения.
Страница расширения на packagist.org:
https://packagist.org/packages/phpunit/dbunit
Подключение.
Подключение в тестирующий класс производится по-разному в зависимости от версии PHPUnit.
Для DBUnit версии 5.7 необходимо тестовый класс наследовать от PHPUnit_Extensions_Database_TestCase:
<?php class AnyTest extends \PHPUnit_Extensions_Database_TestCase { }
для DBUnit версий 6+ нужно подключить трейт:
<?php use PHPUnit\Framework\TestCase; use PHPUnit\DbUnit\TestCaseTrait; class AnyTest extends TestCase { use TestCaseTrait;Подробнее про подключение DBUnit версий 6+ тут.
Описание.
В тестирующем классе обязательно необходимо реализовать два публичных метода — getConnection() и getDataSet().
getConnection() — получение подключения к тестовой бд
getDataSet() — загрузка в базу тестового набора данных (фикстур)
DBUnit использует интерфейс PDO для работы с базой, поэтому подключение, например, через mysqli работать не будет. Хоть метод getConnection() должен использовать PDO для подключения к базе, ваше приложение может использовать любое другое расширение.
Порядок выполнения тестирования DBUnit:
setUp() - подготовка БД в соответствии с данными, получаемыми от метода getDataSet();
testMethod() – тестирование;
tearDown() – очистка системы.
Помимо методов setUp() и tearDown(), которые выполняются перед и после выполнения каждого метода тестирования, добавляются еще setUpBeforeClass() и tearDownAfterClass(), которые выполняются только 1 раз для каждого класса тестов перед созданием объекта класса и после завершения работы класса.
Метод getConnection() для создания подключения с БД использует параметры, которые удобно прописывать в файле phpunit.xml в секции <php>:
<php> <var name="DB_DSN" value="mysql:dbname=test;host=127.0.0.1" /> <var name="DB_USER" value="root" /> <var name="DB_PASSWD" value="" /> <var name="DB_DBNAME" value="test" /> </php>
При загрузке фреймворка PHPUnit, в методе handlePHPConfiguration() класса PHPUnit_Util_Configuration (phpunit\phpunit\src\Util\Configuration.php) файл phpunit.xml парсится и строки элемента var становятся глобальными переменными с названием name и значением value.
Таким образом в методе getConnection() можем извлечь их:
//Установка соединения 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; }
Правилом хорошего тона считается наличие отдельной базы данных для запуска тестов. Таким образом у вас может быть две одноименные таблицы с одинаковой структурой но в разных базах данных.
DbUnit позволяет создавать DataSet`ы (вставку в таблицы данных возвращаемых методом getDataSet), получая данные из различных источников:
Flat Xml — простой способ описание состояния БД в xml-файле, рассчитанный преимущественно на ручное формирование файла.
Xml — полноценный формат задания состояния,более широкие возможности.
MySQL Xml — разновидность предыдущего формата, позволяющая создавать объект DataSet на основании экспорта данных БД утилитой mysqldump. Создание объекта DataSet по текущему состоянию БД.
Flat XML DataSet - наиболее простой вид датасета. Каждый элемент внутри корневого представляет собой одну запись из БД. Имя элемента должно соответствовать имени таблицы, а атрибуты и значения — поля и значения полей соответственно, например:
<?xml version="1.0" encoding="UTF-8" ?> <dataset> <user id="1" name="Serg" email="serg@gmail.com" /> <user id="2" name="Vasya" email="vasya@gmail.com" /> </dataset>
Каждый из вышеперечисленных способов создания наборов таблиц реализуется отдельным методом класса PHPUnit_Extensions_Database_TestCase. Например для Flat Xml используется метод createFlatXMLDataSet:
protected function getDataSet() { return $this->createFlatXMLDataSet(dirname(__FILE__). '/_data/databaseUser.xml'); }
В качестве аргументу нужно передать в виде строки путь к xml-файлу. В данном случае, файл находится в папке _data которая в каталоге с тестами.
PHPUnit автоматически очистит все указанные в xml-файле таблицы и вставит в них данные в порядке, указанном в наборе данных (возвращаемых методом getDataSet). Данный метод вызывается один раз при выполнении метода setUp().
Пример тестирования.
Для примера я написал 2 простых класса, один создает соединение с базой данных, другой использует данное соединения для работы с БД.
Файл app\Db.php:
<?php namespace app; use PDO; class Db { private $connect = null; private $host = '127.0.0.1'; private $db = 'test'; private $user = 'root'; private $pass = ''; private $charset = 'utf8'; public function __construct(array $connect = []) { $connect['host'] ? $this->host = $connect['host'] : NULL; $connect['db'] ? $this->db = $connect['db'] : NULL; $connect['user'] ? $this->user = $connect['user'] : NULL; $connect['pass'] ? $this->pass = $connect['pass'] : NULL; $connect['charset'] ? $this->charset = $connect['charset'] : NULL; } protected function connectionPDO() { $dsn = "mysql:host=$this->host;dbname=$this->db;charset=$this->charset"; $opt = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; $this->connect = new PDO($dsn, $this->user, $this->pass, $opt); } public function getConnect() { if ($this->connect) { return $this->connect; } else { $this->connectionPDO(); return $this->connect; } } }
Файл app/User.php:
<?php namespace app; use app\Db; class User { protected $db; public $name; public $email; public function __construct(Db $db, $data) { $this->db = $db; $this->name = $data['name']; $this->email = $data['email']; } protected function connectDb(){ $con = $this->db->getConnect(); if(is_object($con)){ return $con; } throw new \UnexpectedValueException('Ошибка соединения с БД.'); } public function readAll() { $data = $this->connectDb()->query('SELECT * FROM user'); if ($data) { return $data->fetchAll(); //Возвращаем в виде массива строк } return []; } public function saveNew() { $query = $this->connectDb()->prepare("INSERT INTO user (`name`, `email`) VALUES (?,?) "); $result = $query->execute([$this->name, $this->email]); if ($result) return $result; return false; } }
В классе User есть 2 метода взаимодействующих с базой данных:
- readAll() – получает данные из БД;
- saveNew() – сохраняет данные в БД
их и будем тестировать.
Начнем с настройки тестирования. Добавим в файле конфигурации PHPUnit phpunit.xml секцию <php>.
<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="./vendor/autoload.php" colors="true"> <testsuites> <testsuite name="Application Test Suite"> <directory>tests</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix=".php">app</directory> </whitelist> </filter> <php> <var name="DB_DSN" value="mysql:dbname=test;host=127.0.0.1" /> <var name="DB_USER" value="root" /> <var name="DB_PASSWD" value="" /> <var name="DB_DBNAME" value="test" /> </php> </phpunit>
Как я писал выше, данные указанные в этой секции станут глобальными переменными, которые потом можно использовать в тестовых классах, чтобы не прописывать их в каждом из них.
По данному файлу так же видно, что в секцию bootstrap я подключаю автозагрузчик Composera, чтобы тестируемые классы подключались в тестах автоматически. В приведенных выше классах я использую пространство имен «app», соответственно данные классы находятся в одноименных файлах, которые расположены в каталоге app, ну а тот в корне проекта. В файле composer.json я прописал, что при поиске классов нужно заглядывать в каталог app:
"autoload": { "classmap": ["app"] }
В секции testsuite я указал, что тесты буду размещать в папке tests. Вообще, если вы добрались до DBUnit, то это все уже должны знать, а если подзабыли, то читаем тут.
Итак, строкой
<var name="DB_DBNAME" value="test" />файла phpunit.xml я указал, что тестовая база данных будет называться test, а класс User, который мы будем тестировать, работает с таблицей user. Поэтому создаем данную БД и такую таблицу.
В таблице всего 3 простых поля:
1. id (int + AUTO_INCREMENT)
2. name (varchar(255))
3. email (varchar(255))
Один из тестируемых методов осуществляет выборку из таблицы user. Чтобы протестировать его работу в данной таблице должны быть заполненные строки. Поэтому создаем файл tests/_data/databaseUser.xml:
<?xml version="1.0" encoding="UTF-8" ?> <dataset> <user id="1" name="Serg" email="serg@gmail.com" /> <user id="2" name="Vasya" email="vasya@gmail.com" /> </dataset>данные строки будут автоматически добавляться в БД перед каждым тестом.
Пора написать тестовый класс.
Файл tests/UserTest.php:
<?php namespace tests; use app\Db; use app\User; class UserTest extends \PHPUnit_Extensions_Database_TestCase{ //Подключение к базе private $_conn = null; static $user; //объект нового пользователя //Установка соединения protected function getConnection() { if ($this->_conn === null) { $pdo = new \PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']); $this->_conn = $this->createDefaultDBConnection($pdo, $GLOBALS['DB_DBNAME']); } return $this->_conn; } //Заполнение данными тестовой таблицы из указанного xml-файла protected function getDataSet() { return $this->createFlatXMLDataSet(dirname(__FILE__). '/_data/databaseUser.xml'); } // Сохраняем исходные значения компонентов (тут - user) перед созданием объекта класса с тестами public static function setUpBeforeClass() { $db = new Db(); $user['name'] = 'Name'; $user['email'] = 'name@gmail.com'; self::$user = new User($db, $user); } /* * Тестируем метод readAll() с заполненной таблицей user (с помощью фикстур) */ public function testReadAllGetUsers() { //проверяем, что массив данных из БД не пуст $this->assertNotEmpty(static::$user->readAll()); } /* * При отсутствии данных в БД, метод readAll() должен вернуть пустой массив */ public function testReadAllEmptyUsers() { //Выполняем запрос к БД - очищаем таблицу $this->getConnection()->getConnection()->query('TRUNCATE TABLE `user`'); //кол-во элементов в массиве полученных из БД данных должно быть = 0 $this->assertCount(0, static::$user->readAll()); } /* * Проверяем сохранение нового пользователя в БД, метод saveNew() * с данными указанными в методе setUpBeforeClass() */ public function testSaveNewTrue() { $this->assertTrue(static::$user->saveNew()); } }
Метод getConnection() создает подключение к базе данных используя расширение PDO, а точнее создает объект класса PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection. Т.к. подключение мне еще понадобится, я сохранил этот объект в свойстве $_conn, а в самом методе getConnection() сделал проверку
if ($this->_conn === null)чтобы при повторном запросе подключения не создавать новый объект, а использовать сохраненный в $_conn.
В методе getDataSet(), я указываю из какого файла брать данные для заполнения таблицы перед началом каждого теста.
Метод setUpBeforeClass() выполняется перед созданием объекта самого класса тестов, т.е. до выполнения всех его методов. В нем удобно провести какие-то предварительные работы. В данном примере я создаю объект тестируемого класса и заполняю его конструктор необходимыми данными. Метод статический, поэтому сохранять приходится в статическом свойстве.
Первым тестируем метод readAll() класса User:
public function readAll() { $data = $this->connectDb()->query('SELECT * FROM user'); if ($data) { return $data->fetchAll(); } return []; }он возвращает одно из двух:
- массив данных, если таблица содержат данные;
- пустой массив, если таблица пуста
Тест на получение данных из заполненной таблицы:
public function testReadAllGetUsers() { $this->assertNotEmpty(static::$user->readAll()); }
В данном тесте у объекта User, который создали в методе setUpBeforeClass() вызываем метод readAll(), который просто получает все строки из таблицы. Т.к. перед выполнением теста таблица у нас заполнилась данными из файла databaseUser.xml, проходим проверку успешно – массив данных получен.
Так же можно использовать проверку:
$this->assertArrayHasKey(0, static::$user->readAll());тут мы проверяем, что получен массив у которого есть ключ с нулевым номером, а значит содержится минимум одна строка.
Следующим тестирующим методом нужно проверить действительно ли при отсутствии записей в таблице user, метод readAll() вернет пустой массив:
public function testReadAllEmptyUsers() { $this->getConnection()->getConnection()->query('TRUNCATE TABLE `user`'); $this->assertCount(0, static::$user->readAll()); }
Тут есть небольшая проблема – метод getDataSet() выполняется перед каждым из тестов и каждый раз приводит таблицу в одинаковое состояние, а состояние у нас – две строки из файла databaseUser.xml. Т.е. таблица заполнена, а нам нужно протестировать когда она пустая! Поэтому предварительно я выполняю команду очистки таблицы, используя объект подключения который и был сохранен с этой целью в свойстве $_conn.
Ну а далее выполняем метод readAll() – пытаемся получить данные из пустой таблицы и получаем в результате пустой массив. Чтобы проверить, что это действительно массив нулевой длины (без элементов) удобно использовать проверку
$this->assertCount(0, array);
Строку
$this->assertCount(0, static::$user->readAll());можно переписать используя методы ограничений:
$const = $this->logicalAnd( $this->isType('array'), $this->equalTo([]) ); $this->assertThat(static::$user->readAll(),$const);
Метод readAll() полностью протестирован, теперь переходим к методу saveNew(), который сохраняет данные в таблицу user при создании нового пользователя:
public function saveNew() { $query = $this->connectDb()->prepare("INSERT INTO user (`name`, `email`) VALUES (?,?) "); return $query->execute([$this->name, $this->email]); }
При успешном выполнении запроса по добавлению новой строки с данными, из метода возвращается значение true, в противном случае false.
Нужно проверить, что при передаче верных параметров, данные сохраняются в базу, соответствующий тест:
public function testSaveNewTrue() { $this->assertTrue(static::$user->saveNew()); }
При выполнении метода saveNew() используется объект сформированный в методе setUpBeforeClass() тестирующего класса. Таким образом при прохождении теста можно быть уверенным, что данные сохраняются.
Еще упомяну метод tearDown(). Если при осуществлении тестирования были изменены какие-то внешние компоненты, что вполне вероятно при осуществлении функционального, а не модульного тестирования, данный метод поможет вернуть затронутые компоненты в первоначальное состояние.
Например в каком-то тесте изменили компонент приложения фреймворка Yii2:
Yii::$app->set('название компонента', $newValue);
то по окончания теста нужно вернуть данный компонент в первоначальное состояние, что бы остальные тесты получили компонент в первозданном виде.
Для этого можно сделать так.
1. Предположим, что тест взаимодействует и изменяет компонент приложения «user».
В тестируемом классе создаем защищенное статическое свойство, которое будет хранить ассоциативный массив компонентов, значение которых нужно возвращать в первоначальное состояние:
//компоненты, значения которых нужно восстанавливать перед каждый тестом protected static $_storedEntities = [ 'user' => null, ];В качестве ключей массивов будут названия компонентов, а в качестве значений изначально укажем null.
2. Используем статический метод setUpBeforeClass(), который вызывается только 1 раз перед началом тестирования. В нем, как раз, удобно сохранить начальные значения затрагиваемых компонентов.
public static function setUpBeforeClass() { foreach (static::$_storedEntities as $entity => $value) { static::$_storedEntities[$entity] = Yii::$app->get($entity); } }тут проходимся циклом по массиву компонентов из свойства storedEntities заменяя значение null каждого из компонентов (у нас только один - user) на то значение, которое они содержат до запуска тестов.
3. Используем метод tearDown():
protected function tearDown() { foreach (static::$_storedEntities as $entity => $value) { Yii::$app->set($entity, $value); } }где опять проходимся по массиву компонентов и присваиваем компонентам приложения значения сохраненные до начала тестирования.