Сегодня мы рассмотрим пример создания unit-теста для CMS 1С-Битрикс на примере модуля-заготовки. Вы можете склонировать его репозиторий с моего GitHub.
Как установить модуль, описано в файле справки самого репозитория. Для его установки также потребуется копия сайта на данной CMS (демо-версию можно бесплатно скачать с официального сайта и установить на каком-либо локальном сервере или, собственно, задействовать хостинг/VPS).
Перед тем как перейти к деталям, хотелось бы высказать пару мыслей. Некоторые разработчики модулей на данной платформе, по моим наблюдениям, не используют unit-тесты в принципе. Но подумайте вот о чем — если вы выкатываете новый функционал, вы всегда рискуете сломать старый и даже этого не заметить. Чтобы это предотвратить, вы можете вручную тестировать весь возможный функционал полностью, после каждой правки. Или вы можете автоматизировать часть ручного труда и использовать модульные, а также интеграционные тесты. По мере роста приложения вы будете экономить все больше времени, так как вам не придется проверять все аспекты вручную. Конечно, модульные тесты это не панацея, но, как считается, без них чистый код не возможен в принципе. Еще представьте, что ваш модуль приобретен несколькими десятками клиентов (не говоря о сотнях). При этом цена ошибок возрастает, но с авто-тестами их намного легче минимизировать.
Ну а теперь, перейдем к примеру реализации тестов, использованном в упомянутом выше репозитории. Отмечу важные моменты:
- Для того, чтобы не было проблем с статическими методами классов из ядра Битрикс при создании тестовых двойников (mock-объектов), мы сделаем над ними классы-обертки. Их можно видеть в директории lib/Wrappers.
- На примере класса lib/Example.php видно, что внедрение зависимостей происходит через конструктор. Благодаря такой схеме, мы можем заглушить любой используемый класс Битрикс в тестируемом методе.
Давайте рассмотрим детально сам тест в фаиле tests/unit/ExampleTest.php:
namespace Somefirm\Emptymodule;
use Bitrix\Main\Loader;
class ExampleTest extends BitrixTestCase
{
public function testExampleMethod()
{
// Входные параметры
// Подготовка параметров для тестируемого метода,
// в данном случае используем пустой массив
$param = [];
// Результат для проверки
// Эталон, с которым будем сравнивать результат работы функции
$expectedResult = [1, 2];
// Заглушки
// Тут мы подключаем модуль Битрикс iblock, потому что дальше
// мы используем CIBlockResult для заглушки
// Этого класса нет в Wrappers, он и без обертки работает нормально
Loader::includeModule('iblock');
// Создаем заглушку для результата ответа CIBlockElement
$CIBlockResultStub = $this->createMock(\CIBlockResult::class);
// Тут мы видим массив, элементы которого будут отдаваться при
// каждой итерации при вызове Fetch() в цикле
// Последний элемент — false, то есть необходимо завершить цикл
$fetchResults = [
[
'ID' => 1,
],
[
'ID' => 2,
],
false,
];
// Тут мы создаем заглушку метода Fetch()
// С помощью метода onConsecutiveCalls() мы сообщаем PHPUnit,
// что метод должен последовательно вернуть значения массива $fetchResults
// при каждом новом вызове функции Fetch()
$CIBlockResultStub->method('Fetch')
->will($this->onConsecutiveCalls(...$fetchResults));
// Создаем заглушку CIBlockElement (через обертку)
$CIBlockElementStub = $this->createMock(Wrappers\CIBlockElement::class);
// И добавляем этой заглушке метод GetList(), который должен вернуть
// другую заглушку $CIBlockResultStub, созданную выше
$CIBlockElementStub->method('GetList')
->willReturn($CIBlockResultStub);
// Вычисление результата
// Внедряем подготовленную заглушку через конструктор класса Example
$object = new Example([
'CIBlockElement' => $CIBlockElementStub,
]);
// Имитируем работу тестируемого метода exampleMethod()
$result = $object->exampleMethod($param);
// Проверка
// Сравнение эталона с реальным результатом работы функции
// Если результаты совпадут, то тест пройден
$this->assertEquals($expectedResult, $result);
}
}
Итак, из кода выше видно, что мы не просто написали простейший тест, но изолировали реальные классы Битрикс, которые использует тестируемая функция. Делается это для того, чтобы тесты не делали запросы к БД и чтобы они не зависели от наличия нужных данных. Если при рефакторинге этой функции мы случайно что-то сломаем, об этом быстро сообщит unit-тест. А когда покрытие программы тестами достигает значительной величины, наша уверенность в надежности кода существенно возрастает.
Кстати, если отключить заглушки, у нас получится уже интеграционный тест, который тестирует не только текущую функцию, но и ее интеграцию с Битрикс. Однако, для таких тестов необходимо подготовить необходимое состояние БД и потом, при необходимости, вернуть обратно в состояние, которое было до начала теста. Для этого нужно написать соответствующий код, который сделает это перед/после запуска теста. Но это уже выходит за рамки этой статьи.