Организация PHP-тестов с большими массивами данных
При написании тестов мы сравниваем данные, возвращаемые тестируемой функцией, с их ожидаемыми значениями. Действительные значения мы получаем из результата вызова функции, а ожидаемые значение традиционно указываем в коде теста. Зачастую ожидаемое значение является массивом, а иногда очень большим массивом. Кроме того, тестируемая функция может требовать большой массив данных в качестве входного параметра. И все эти большие массивы должны так или иначе присутствовать в коде теста.
Так, при разработке API нашей команде довелось сверять в тестах многомерные массивы, которые занимают несколько экранов. Наличие этих массивов в коде теста сделало бы разработку и чтение тестов крайне неудобными, поэтому появилась необходимость вынести тестовые данные из кода теста. При этом вынести их надо недалеко от самого теста, чтобы разработчику было удобно просматривать и редактировать эти данные.
В результате был написан скрипт, который позволяет извлекать массивы данных из php-файлов, а также из обычных текстовых файлов, и подставлять эти данные в код теста. Позже этот скрипт был оформлен в php-пакет test-data-provider.
Пакет содержит один класс 'Yuraplohov\TestDataProvider\Provider' с тремя публичными методами:
Provider::getPHPUnitCases(array $caseDirs): array - извлекает массив тестовых кейсов в формате фреймворка PHPUnit.
Provider::getCodeceptionCases(array $caseDirs): array - извлекает массив тестовых кейсов в формате фреймворка Codeception.
Provider::get(string $path): mixed - метод, работающий с любым php-фреймворком для написания тестов. Извлекает данные из указанной директории или конкретного файла. Также метод позволяет извлекать конкретный элемент массива из php-файла.
Все три метода требуют наличия директории 'data' на одном уровне с файлом теста. В 'data' и хранятся файлы с тестовыми данными.
Для методов getPHPUnitCases() и getCodeceptionCases() структура директории с тестами может быть такой:
tests
Example_Test
ExampleTest.php
data
case_1
input.json
expected.php
case_2
input.json
expected.php
На одном уровне с 'ExampleTest.php' находится директория 'data'. В 'data' помещаются директории с конкретными тестовыми кейсами (case_1, case_2). Названия для директорий с кейсами можно выбирать произвольные, но директория 'data' всегда должна иметь такое название.
Файлом с тестовыми данными может быть php-файл, из которого возвращается массив:
<?php
return [
// Большой массив
];
Кроме того, все три метода позволяют извлекать данные из любых текстовых файлов (например json, xml, txt). Содержимое таких файлов возвращается в виде строки без каких-либо преобразований.
Методы getPHPUnitCases() и getCodeceptionCases() на вход принимают массив названий директорий с тестовыми кейсами. В рассматриваемой структуре это массив ['case_1', 'case_2'].
Пример использования метода getPHPUnitCases():
<?php
/**
* @test
* @dataProvider someProvider
*/
public function it_gets_some_result(array $case)
{
$sut = new SomeClass;
$this->assertEquals($case['expected'], $sut->someMethod($case['input']));
}
/**
* @return array
*/
public function someProvider(): array
{
return (new Provider)->getPHPUnitCases([
'case_1',
'case_2',
]);
}
Пример использования метода getCodeceptionCases():
<?php
/**
* @param UnitTester $I
* @param \Codeception\Example $example
* @dataProvider someProvider
*/
public function it_gets_some_result(UnitTester $I, \Codeception\Example $example)
{
$sut = new SomeClass;
$I->assertEquals($example['expected'], $sut->someMethod($example['input']));
}
/**
* @return array
*/
protected function someProvider()
{
return (new Provider)->getCodeceptionCases([
'case_1',
'case_2',
]);
}
Третий метод get() позволяет работать с любым PHP-фреймворком для тестов (PHPUnit, Codeception, Pest).
В качестве параметра в данный метод можно передать путь к директории, путь к файлу или путь к элементу массива в php-файле. При этом директории и файл разделяются слешем ("/"), а уровни вложенности массива разделяются точкой (".").
Рассмотрим такой пример структуры директории с тестами:
tests
Example_Test
ExampleTest.php
data
service
input.json
settings.php
expected.php
Директория 'data' также находится на одном уровне с файлом теста.
Варианты вызова метода get() для рассматриваемой структуры:
get('service');
- извлекает содержимое всех файлов в директории 'data/service'
get('expected');
- извлекает содержимое файла 'data/expected.php'
get('service/input');
- извлекает содержимое файла 'data/service/input.json'
get('service/settings');
- извлекает содержимое файла 'data/service/settings.php'
get('service/settings.foo.bar');
- извлекает значение элемента 'bar' в массиве, возвращаемом файлом 'settings.php'. При этом 'foo.bar' - это иерархия элементов массива.
Пример использования метода get():
<?php
/** @test */
public function it_gets_some_result()
{
$dp = new Provider;
$sut = new SomeClass($dp->get('service/settings'));
$this->assertEquals($dp->get('expected'), $sut->someMethod($dp->get('service/input')));
}
С внедрением данного пакета на проекте было принято соглашение о том, что если из теста ExampleTest.php необходимо вынести тестовые данные, то создается директория Example_Test, в которую кладутся сам файл теста и директория data. Разумеется, в директории Example_Test помимо ExampleTest.php не должно быть других тестовых файлов.
Как уже было сказано, директориям с конкретными кейсами можно давать любые названия, желательно более-менее осмысленные, раскрывающие особенность кейса. Чтобы отсортировать директории с кейсами, им можно давать названия с номером в качестве префикса ('01_some_interesting_case', '02_another_case' ...).
Кроме того, при необходимости в каждую директорию кейса можно поместить специальный файл (например 'case.txt') с кратким однострочным описанием кейса, чтобы использовать это описание в качестве параметра 'message' в assert-методах.