Pull to refresh

PHPUnit и его Database Extension. Беглый взгляд

PHP *

Пространное и многословное вступление


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

Некоторые тесты не используются ввиду того что тестируемый функционал так и не вышел в продакшн и на годы осел в файлах и директориях, а некоторые просто всех утомили тем хаосом который там был сотворен. Вся та произведенная нами на свет энтропия стала результатом нашей некомпетентности в модульном тестировании и не системного подхода к их внедрению. В наших тестах нарушены практически все принципы модульного тестирования. Начиная с того что они зависят один от другого заканчивая тем что в них можно увидеть разбухшие от подготовительной работы тестовые методы пестрящие sql-синтаксисом и многое другое… Плюс ко всему выше названному, почти все предыдущие попытки внедрения модульного тестирования были в большей мере самодеятельностью не особо санкционированной заказчиком, потому как только происходил затык тесты бросались и функционал начинал писаться в отрыве от тестов, и стоит ли говорить о том что к дописыванию тестов возвращаться не собирался никто.



Плавный переход к практической стороне вопроса


Это все лирика. Теперь пойдет больше по делу. Как можно понять проект написан на php, не маленький и живет уже по меньшей мере больше года(а в действительности 4 года).

Настали времена когда даже самые упрямые и «технически грамотные» заказчики стали понимать необходимость в качестве кода на котором так настаивают сочувствующие PM-ы. Сверху пришла «указивка» заняться модульным тестированием по настоящему. Даже задачи в жиру положили соответствующие.

Итак основываясь на нашем опыте прежних неудач мы выделили ряд проблем:
  1. Проблема фикстур. Не все что берется из базы можно заменить
    моками. Связанно это с некоторой безсистемностью работы с базой. У нас нет ORM и все функции последнего между делом выполняет модель.
  2. Скорость прогона тестов. Последняя попытка внедрения захлебнулась
    отчасти из-за этого, а отчасти и …
  3. Поддерживаемость. … из-за неподдерживаемости причиной которой
    стала проблема из п.1 решая которую мы оснастили наши тесты
    многострочными инсертами и апдейтами.

Мы обратились к компетентному в этом вопросе коллеге — Java-разработчику. Он то нам и поведал о таких инструментах как JUnit и DbUnit. Он же нам и рассказал о процессе который они используют с участием этого инструментария: во время разработки они прогоняют тесты на in memory базах данных, решая таким образом задачу скорости работы тестов увеличивая вероятность их более частого запуска, перед коммитами они запускают те же тесты на реальной базе и потом еще добивают баги continious integration системой.

xUnit фреймворк(SimpleTest) мы использовали и ранее, а вот о DbUnit мы слышали впервые. Собственно о нем и пойдет речь далее. Для php есть реализация DbUnit в виде расширения к PHPUnit. Смысл его использования в том что все фикстуры готовятся в виде xml-файлов и заливаются в тестовую базу перед запуском каждого тестового метода. Это по моему мнению решает задачу поддерживаемости фикстур, потому как все же xml более читаемый чем sql-дампы.

Практический пример


Предположим есть у нас таблица person
  1. CREATE TABLE person (
  2.      id INTEGER PRIMARY KEY AUTOINCREMENT,
  3.      name VARCHAR(255),
  4.      surname VARCHAR(255)
  5. );


Для того что бы что-то в ней тестировать нужно что бы она была наполнена какими-то данными. В Database Extension PHPUnit наполнение описывается в xml формате, по одному элементу на каждую строку в таблице. Имя элемента должно соответствовать имени таблицы, а имена атрибутов — именам полей таблицы.

Создадим такой себе persons.xml с фикстурой для этой таблицы:
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <dataset>
  3.     <person
  4.         id="1"
  5.        name="Nalabal"
  6.        surname="Nadjava"/>
  7. </dataset>



Теперь внимание тест кейс:
  1. require_once 'PHPUnit/Extensions/Database/TestCase.php';
  2. require_once 'PHPUnit/Framework.php';
  3. class PersonTest extends PHPUnit_Extensions_Database_TestCase
  4. {
  5.     public function __construct()
  6.     {
  7.         $this->connection = new PDO('sqlite::memory:');
  8.         $this->connection->query("
  9.            CREATE TABLE person (
  10.                id INTEGER PRIMARY KEY AUTOINCREMENT,
  11.                name VARCHAR(255),
  12.        surname VARCHAR(255)
  13.    );
  14.        ");
  15.  
  16.     }
  17.     protected function getConnection()
  18.     {
  19.         return $this->createDefaultDBConnection($this->connection, 'sqlite');
  20.     }
  21.  
  22.     protected function getDataSet()
  23.     {
  24.         return $this->createFlatXMLDataSet(dirname(__FILE__).'/persons.xml');
  25.     }
  26. }


Очевидно — тест ничего не тестирует, но что то там мы уже понакодили:
Он унаследован от PHPUnit_Extensions_Database_TestCase в котором уже реализованы некоторые методы, в том числе setUp (очищает базу и заполняет ее вновь) и tearDown — который делает тоже самое. В доке по PHPUnit более детально расписано, то как эти методы взаимодействуют с тестовыми методами. В конструкторе для простоты я создаю вышеуказанную табличку и в inmemory базе данных. После я переопределяю два абстрактных метода Database_TestCase. Первый возвращает объект соединения с базой, а второй объект созданный из нашей xml-фикстуры. Эти методы используются в определенных в Database_TestCase setUp и tearDown методах.

Теперь проверим, добавив новый метод
  1. public function testPerson ()
  2. {
  3.     $sql = "SELECT * FROM person";
  4.     $statement =
  5.         $this->getConnection()->getConnection()->query($sql);
  6.     $result = $statement->fetchAll();
  7.     $this->assertEquals(1, sizeof($result));
  8.     $this->assertEquals('Nalabal', $result[00]['name']);
  9. }


Результат: OK (1 test, 2 assertions)
В методе выбираем все записи из таблицы persons и проверяем выборку на соответствие нашей фикстуре

Мне в конкретном случае понадобилось добавлять некоторые данные во время выполнения конкретного тестового метода, не найдя ничего в доке я поковырялся слегонь в коде и пришел к следующему решению:

Добавляем новую фикстуру additionalPersons.xml
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <dataset>
  3.     <person
  4.        id="2"
  5.        name="Nageneril"
  6.        surname="Mudrapragram"/>
  7.     <person
  8.        id="3"
  9.        name="Strudomprassal"
  10.        surname="Vashapragram"/>
  11. </dataset>


И пишем такой тестовый метод:
  1. public function testAdditionalPerson ()
  2. {
  3.     $insertOperation = PHPUnit_Extensions_Database_Operation_Factory::INSERT();
  4.     $insertOperation->execute($this->getConnection(), $this->createFlatXMLDataSet(dirname(__FILE__).'/additionalPersons.xml'));
  5.     $sql = "SELECT * FROM person";
  6.     $statement = $this->getConnection()->getConnection()->query($sql);
  7.     $result = $statement->fetchAll();
  8.     $this->assertEquals(3, sizeof($result));
  9.     $this->assertEquals('Nalabal', $result[00]['name']);
  10.     $this->assertEquals('Nageneril', $result[1]['name']);
  11.     $this->assertEquals('Strudomprassal', $result[2]['name']);
  12. }


В 3й строке создаем с помощью Operation_Factory (вопрос с Операциями в официальной доке пока еще не описан) объект вставки который реализовывает интерфейс PHPUnit_Extensions_Database_Operation_IDatabaseOperation у которого есть только один метод имеющий следующую сигнатуру:

  1. public function execute(PHPUnit_Extensions_Database_DB_IDatabaseConnection $connection, PHPUnit_Extensions_Database_DataSet_IDataSet $dataSet);


Его то мы и вызываем в 4й строке предыдущего листинга, а он в свою очередь дополняет нашу первоначальную фикстуру из persons.xml данными из additionalPersons.xml

Результатом запуска будет: OK (2 tests, 6 assertions)

Заключение


Вероятно что в жизни все не так сложно как в этом примере. Потому прошу не поддавать искусственность описываемого критике.
По мере развития ситуации мне хотелось бы посвящать Вас во все допустимые детали как возможность запомнить все раскопанное и неописанное в документации.
Конечно же, только при условии, что это кому-нибудь нужно(это я в скором времени пойму).
Tags: phpunit
Hubs: PHP
Total votes 26: ↑23 and ↓3 +20
Comments 29
Comments Comments 29