При работе с фреймворком для модульного тестирования PHPUnit, для удобства тестирования методов работающих с базой данных (создания функциональных тестов), существует расширение DBUnit.
Основная задача, которую решает данное расширение, это автоматическое и удобное приведение базы данных в требуемое состояние перед каждым тестом. Т.е. наполнение таблиц нужными данными (фикстурами).

Установка.


В зависимости от используемой версии 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);
        }
    }
где опять проходимся по массиву компонентов и присваиваем компонентам приложения значения сохраненные до начала тестирования.