Предисловие
Говоря о разработке сайтов с использованием CMS 1C Bitrix вопрос покрытия тестами поднимается редко. Главная причина в том, что большинство проектов обходится штатным функционалом, который предоставляется системой - его сложно (да и, в общем-то, незачем) тестировать.
Но со временем проект разрастается, появляется необходимость интеграции со сторонними сервисами и службами (платежные системы, API служб доставки и другие), либо же разрабатывается все более и более специализированный функционал. И чем дальше, тем больше объем кода, контроль за которым лежит уже на разработчике.
Это и является предпосылкой для внедрения в CMS механизма тестирования.
Процесс подготовки окружения к написанию тестов состоит из нескольких шагов:
установить Composer;
настройка Bitrix для работы с Composer;
установить PHPUnit;
настроить PHPUnit для работы с Bitrix.
Composer
Установка
Установку composer проводим по инструкции.
cd ~ curl -sS https://getcomposer.org/installer -o composer-setup.php HASH=Хеш файла php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
По завершению - в консоли видим сообщение, что установщик скачан успешно:
Installer verified
Переходим к установке:
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
По окончанию видим сообщение о успешной установке:
Output All settings correct for using Composer Downloading... Composer (version 2.1.9) successfully installed to: /usr/local/bin/composer Use it: php /usr/local/bin/composer
Всю работу с зависимостями организуем в каталоге local. Инициализируем проект:
cd local composer init
Указываем нужные параметры, подтверждаем.
По завершению у нас появляется файл /local/composer.json с примерно таким содержимым:
{ "name": "myproject/website", "type": "project", "authors": [ { "name": "Andriy Kryvenko", "email": "krivenko.a.b@gmail.com" } ] }
Теперь скажем битриксу, что надо использовать сторонние пакеты, установленные через Composer.
Открываем файл /local/php_interface/init.php (создаем, если не существует) и подключаем файл autoload:
<?php include_once(__DIR__.'/../vendor/autoload.php');
После этого скрываем каталог /local/vendor/ от системы контроля версий. В файл .gitignore добавляем /local/vendor/*
PHPUnit
Переходим к установке PHPUnit. На прод сервере он нам не нужен, поэтому устанавливаем только в качестве dev зависимости и создаем конфиг-файл. Для этого выполняем в командной строке:
composer require --dev phpunit/phpunit ^9.0 ./vendor/bin/phpunit --generate-configuration
В dev-зависимости был добавлен phpunit, а так же создан файл /local/phpunit.xml
Помимо непосредственно PHPUnit, для более приятного вида результатов тестов я использую пакет sempro/phpunit-pretty-print.
composer require --dev sempro/phpunit-pretty-print ^1.4
Теперь нужно создать файл, который будет использоваться при тестировании для инициализации ядра продукта. Назовем его /local/tests/bootstrap.php
<?php define("NOT_CHECK_PERMISSIONS", true); define("NO_AGENT_CHECK", true); $_SERVER["DOCUMENT_ROOT"] = __DIR__ . '/../..'; require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
Настроим PHPUnit, чтобы использовался наш файл инициализации и наш класс декорации результатов. Откроем файл /local/phpunit.xml и приведем его к следующему виду:
phpunit.xml
<?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd" bootstrap="tests/bootstrap.php" cacheResultFile=".phpunit.cache/test-results" colors="true" printerClass="Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9" executionOrder="random" forceCoversAnnotation="true" beStrictAboutCoversAnnotation="true" beStrictAboutOutputDuringTests="true" beStrictAboutTodoAnnotatedTests="true" convertDeprecationsToExceptions="true" failOnRisky="true" failOnWarning="true" verbose="true"> <php> <ini name="memory_limit" value="-1"/> <ini name="display_errors" value="true"/> </php> <testsuites> <testsuite name="default"> <directory suffix="Test.php">tests</directory> <exclude>tests/Stubs</exclude> <exclude>tests/Request</exclude> <exclude>tests/Response</exclude> </testsuite> </testsuites> <coverage cacheDirectory=".phpunit.cache/code-coverage" processUncoveredFiles="true"> <include> <directory suffix=".php">classes</directory> </include> </coverage> </phpunit>
И добавим команду для быстрого запуска тестов
composer.json
{ "name": "myproject/website", "type": "project", "authors": [ { "name": "Andriy Kryvenko", "email": "krivenko.a.b@gmail.com" } ], "require-dev": { "phpunit/phpunit": "^9", "sempro/phpunit-pretty-print": "^1.4" }, "scripts": { "test": "phpunit" } }
Теперь при выполнении команды
composer test
будут запускаться все тесты. На этом процесс настройки закончен и можно переходить к написанию тестов.
Перед тем, как продолжить
Для удобства свои классы лучше размещать в каталоге local/classes, примерно в следующем виде:
/local/classes/MyProject/Product.php /local/classes/MyProject/Rests.php
И указать в файле /local/composer.json в секции autoload путь к каталогу:
{ "name": "myproject/website", "type": "project", "authors": [ { "name": "Andriy Kryvenko", "email": "krivenko.a.b@gmail.com" } ], "require-dev": { "phpunit/phpunit": "^9", "sempro/phpunit-pretty-print": "^1.4" }, "scripts": { "test": "phpunit" }, "autoload": { "psr-4": { "": "./classes/" } } }
Пример теста
В качестве примера я покажу реальную ситуацию, ее решение и тесты, которые это решение покрывают (часть кода, не относящуюся непосредственно к преобразованиям, в пример не включаю).
Собственно, ситуация: из 1С на сайт в виде строк передается информация о доступных сроках поставки товара, например:
24 часа-7|до 2 дней-14|до 15 дней-неогр
При покупке до 7 штук - поставим за 24 часа, до 14 штук - за 2 дня, в другом случае - за 15 дней.
Нужно преобразовать их в объекты Leftover для дальнейшего использования. Преобразование выполняем с помощью LeftoverTransformer:
Leftover
<?php namespace MyProject\Product\Requisites; class Leftover { public int $time = 0; /** * Доступное количество для данного интервала. * Если количество равно -1.0 - то подразумеваем, что товара неограниченное количество */ public float $quantity = 0.0; public function __construct(int $time, float $quantity) { $this->time = $time; $this->quantity = $quantity; } public function isAvailable(): bool { return ($this->quantity > 0 || $this->quantity == -1.0); } }
LeftoverTransformer
<?php namespace MyProject\Product\Requisites\Transform; use MyProject\Product\Requisites\Leftover; class LeftoverTransformer { /** * @param string $leftoversString * @return Leftover[] * Строку получаем в виде * 24 часа-7|до 2 дней-14|до 7 дней-22|до 15 дней-неогр */ public static function transform(string $leftoversString): array { $leftovers = []; $intervals = explode('|', $leftoversString); foreach ($intervals as $v){ $interval = explode('-', $v); $intervalValues = []; foreach ($interval as $k => $part) { $intervalValues[] = trim($part); } if (!empty($intervalValues[0]) && !empty($intervalValues[1])) { $leftovers[] = new Leftover( self::getTimeFromString($intervalValues[0]), self::getQuantityFromString($intervalValues[1]) ); } } return $leftovers; } private static function getTimeFromString(string $timeString): int { switch ($timeString) { case '24 часа': $time = 1; break; default: $parts = explode(' ', $timeString); $time = intval($parts[1]); break; } return $time; } private static function getQuantityFromString(string $quantityString): int { switch ($quantityString) { case 'неогр': $quantity = -1; break; default: $quantity = intval($quantityString); break; } return $quantity; } }
И покрываем эти классы соответствующими тестами:
LeftoverTest
<?php namespace MyProject\Product\Requisites; use PHPUnit\Framework\TestCase; /** * @covers Leftover */ class LeftoverTest extends TestCase { public function testIsAvailable(): void { $leftover = new Leftover(1, 12.0); $this->assertTrue($leftover->isAvailable()); } public function testAvailableUnlimited(): void { $leftover = new Leftover(1, -1.0); $this->assertTrue($leftover->isAvailable()); } public function testUnavailable(): void { $leftover = new Leftover(1, 0.0); $this->assertFalse($leftover->isAvailable()); } }
LeftoverTransformerTest
<?php namespace MyProject\Product\Requisites\Transform; use PHPUnit\Framework\TestCase; /** * @covers LeftoverTransformer */ class LeftoverTransformerTest extends TestCase { public function testEmpty(): void { $this->assertEmpty(LeftoverTransformer::transform('')); } public function testLeftoversCount(): void { $leftoverString = '24 часа-7|до 2 дней-14|до 7 дней-22|до 15 дней-неогр'; $leftovers = LeftoverTransformer::transform($leftoverString); $this->assertCount(4, $leftovers); } /** * @param string $leftoverString * @param int $expectedTime * @param float $expectedQuantity * @return void * @dataProvider leftoversProvider */ public function testLeftovers(string $leftoverString, int $expectedTime, float $expectedQuantity): void { $leftovers = LeftoverTransformer::transform($leftoverString); $this->assertEquals($expectedTime, $leftovers[0]->time); $this->assertEquals($expectedQuantity, $leftovers[0]->quantity); } public function leftoversProvider(): array { return [ '24 часа-7' => [ '24 часа-7', 1, 7.0 ], 'до 2 дней-14' => [ 'до 2 дней-14', 2, 14.0 ], 'до 7 дней-22' => [ 'до 7 дней-22', 7, 22.0 ], 'до 15 дней-неогр' => [ 'до 15 дней-неогр', 15, -1.0 ] ]; } }
Приведенный пример теста позволяет быть уверенным в том, что при работе с товарами мы всегда точно знаем, доступно ли к покупке то или иное количество товара и в какой срок.
upd
По совету из комментариев убрал ручную регистрацию автозагрузки и указал путь к классам в /local/composer.json
