При написании тестов мы сравниваем данные, возвращаемые тестируемой функцией, с их ожидаемыми значениями. Действительные значения мы получаем из результата вызова функции, а ожидаемые значение традиционно указываем в коде теста. Зачастую ожидаемое значение является массивом, а иногда очень большим массивом. Кроме того, тестируемая функция может требовать большой массив данных в качестве входного параметра. И все эти большие массивы должны так или иначе присутствовать в коде теста.

Так, при разработке API нашей команде довелось сверять в тестах многомерные массивы, которые занимают несколько экранов. Наличие этих массивов в коде теста сделало бы разработку и чтение тестов крайне неудобными, поэтому появилась необходимость вынести тестовые данные из кода теста. При этом вынести их надо недалеко от самого теста, чтобы разработчику было удобно просматривать и редактировать эти данные.

В результате был написан скрипт, который позволяет извлекать массивы данных из php-файлов, а также из обычных текстовых файлов, и подставлять эти данные в код теста. Позже этот скрипт был оформлен в php-пакет test-data-provider.

Пакет содержит один класс 'Yuraplohov\TestDataProvider\Provider' с тремя публичными методами:

  1. Provider::getPHPUnitCases(array $caseDirs): array - извлекает массив тестовых кейсов в формате фреймворка PHPUnit.

  2. Provider::getCodeceptionCases(array $caseDirs): array - извлекает массив тестовых кейсов в формате фреймворка Codeception.

  3. 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-методах.

Комментарии (2)


  1. xxxphilinxxx
    19.03.2022 17:26
    +3

    А готовые решения искали? Чем не подошли? Для PHPUnit, например, есть https://github.com/redaxmedia/phpunit-provider-autoloader, который и структуру зафиксирует (соглашения - дело не очень надежное), и избавит от копипасты.


    1. YuriiPlohov Автор
      19.03.2022 17:35
      -1

      Спасибо за ссылку. Не видел этот пакет. Сначала писал скрипт для Codeception, а потом уже и искать что-либо не стал