Пример использования PHPUnit в своем модуле для CMS 1C-Битрикс

Сегодня мы рассмотрим пример создания unit-теста для CMS 1С-Битрикс на примере модуля-заготовки. Вы можете склонировать его репозиторий с моего GitHub.

Как установить модуль, описано в файле справки самого репозитория. Для его установки также потребуется копия сайта на данной CMS (демо-версию можно бесплатно скачать с официального сайта и установить на каком-либо локальном сервере или, собственно, задействовать хостинг/VPS).

Перед тем как перейти к деталям, хотелось бы высказать пару мыслей. Некоторые разработчики модулей на данной платформе, по моим наблюдениям, не используют unit-тесты в принципе. Но подумайте вот о чем — если вы выкатываете новый функционал, вы всегда рискуете сломать старый и даже этого не заметить. Чтобы это предотвратить, вы можете вручную тестировать весь возможный функционал полностью, после каждой правки. Или вы можете автоматизировать часть ручного труда и использовать модульные, а также интеграционные тесты. По мере роста приложения вы будете экономить все больше времени, так как вам не придется проверять все аспекты вручную. Конечно, модульные тесты это не панацея, но, как считается, без них чистый код не возможен в принципе. Еще представьте, что ваш модуль приобретен несколькими десятками клиентов (не говоря о сотнях). При этом цена ошибок возрастает, но с авто-тестами их намного легче минимизировать.

Ну а теперь, перейдем к примеру реализации тестов, использованном в упомянутом выше репозитории. Отмечу важные моменты:

  1. Для того, чтобы не было проблем с статическими методами классов из ядра Битрикс при создании тестовых двойников (mock-объектов), мы сделаем над ними классы-обертки. Их можно видеть в директории lib/Wrappers.
  2. На примере класса 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-тест. А когда покрытие программы тестами достигает значительной величины, наша уверенность в надежности кода существенно возрастает.

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

Переход с Notepad++ на VS Code

Долгие годы верой и правдой служил Notepad++. Доходило до того что я использовал его даже в Linux (через Wine). Несмотря на до сих пор его высокую популярность, все же захотел перейти на VS Сode, и не пожалел. Все таки в Notepad++ несмотря на множество плагинов, функционал маловат и продуктивность из-за этого падает. VS Code же напротив сейчас на гребне волны, и даже из коробки в нем много чего есть. Чего стоит только тесная интеграция с git. Хотя зачастую по старой памяти параллельно открыта обычная консоль git, и кое что все равно приходится делать там. Но, тем не менее, наиболее частые операции — add, commit, checkout, push — теперь можно делать прямо из редактора. Однако трудности перехода все же были. Конечно, они в какой-то мере специфичны для меня, но возможно некоторые решения пригодятся и вам. Перед тем как перейти к деталям, замечу, что установка плагинов в VS Code вынесена на основную панель, а не запрятана в недрах меню (в добавок с недвусмысленной иконкой — собери все из кубиков).

Ну а теперь по порядку:

Метки на полях, с быстрым переходом по ним

В Npp они ставились ctrl+F2 или просто мышкой на полях. Переход по ним через F2. Так как бывает приходится иметь дело с длинными файлами, а так же редактировать параллельно код в двух противоположных концах файла, эта функция очень удобна.

Для VS Code есть несколько плагинов на эту тему. Есть например Numbered Bookmarks, который даже судя по названию позволяет ставить нумерованные метки, но для меня это лишне. Остановился на просто Bookmarks. По умолчанию там ctrl+alt+K — создать метку, ctrl+alt+J переход на предыдущую, а ctrl+alt+L на следующую. Метки сохраняются в папке мета-информации .vccode (это нужно иметь ввиду при отслеживании файлов в git).

Вставка HTML

В Notepad++ у меня за это отвечал плагин WebEdit с настроенными кнопками употребительных тегов на панели. Плагин одинаково хорошо вставляет и оборачивает код.

Для VS Code для простой вставки тега, достаточно написать его название и нажать Enter (зависит от типа файла, и соответствующего авто-завершения). Обертку кода в тег тоже можно сделать без плагинов. Для этого выделяем код для обертки, нажимаем shift+ctrl+P (это универсальная комбинация для вызова многих команд), выбираем действие Emmet: Wrap Individual Lines with Abbreviation (Скорее всего оно будет первое в списке, то есть просто нажимаем Enter), затем пишем название тега и еще раз Enter. Довольно мудрено, поэтому я стал искать плагин. Мне пришелся по душе htmltagwrap. Здесь горячая клавиша по умолчанию — alt+W и сразу же происходит обертка тегом P. Если нужен другой тег, просто пишете его название, например div и тег меняется на нужный. К сожалению при нескольких уровнях вложенности иногда бывает работает не корректно и не прописывается автоматически имя завершающего тега, возможно это исправят позднее.

Определение кодировки

Увы, еще встречается кодировка отличная от UTF-8 кое где, например в том же Bitrix. И нужно корректное определение файлов с ней. Кстати, Notepad++ тоже не всегда корректно определял кодировку и мог спутать windows-1251 с Macintosh.

Чтобы VS Code определял кодировку при открытии файла, нужно включить опцию autoGuessEncoding. Для этого идем в меню File -> Preferences -> Settings. Вводим в поиске эту опцию и ставим галку. Теперь редактор должен определять кодировку сам, иначе придется выбирать ее вручную, нажав на название кодировки справа внизу экрана.

Прочее

По умолчанию стоит темная тема оформления. Но я привык к светлой, возможно и вы захотите выбрать что то иное. Для выбора темы нужно нажать не совсем типичную комбинацию клавиш ctrl+K+T, затем выбрать нужную в выпадающем списке. Мне понравилась предустановленная тема Light (Visual Studio). Для особых ценителей можно установить через плагины тему NotepadPlusPlus Remixed Theme. Видимо создана тем, кто так же переходил с Npp.

Хотя в этой статье не ставится цель осветить все полезные плагины, а лишь те что помогают плавно перейти с Npp, упомяну еще парочку.

  • Git History — позволяет посмотреть историю коммитов, подобно Git GUI. На панели справа вверху появляется иконка для вызова плагина.
  • WakaTime — тайм-трекер. Ведет подробную статистику времени, которое вы провели за кодингом. Требуется регистрация на одноименном сайте — wakatime.com — и последующий ввод Api Key в настройках плагина. Для этого нажимаем универсальное shift+ctrl+p, выбираем WakaTime: Api Key, и вставляем ключ полученный на сайте. Саму статистику можно посмотреть на сайте, кликнув на времени, которое отследил Waka, на нижней панели в редакторе.

Выпадающее меню на CSS без доступа к HTML, без JavaScript

В некоторых онлайн конструкторах сайтов на определенных тарифах очень ограничен функционал (например — Битрикс24). Бывает нет возможности вставить свой JavaScript-код, а так же поменять HTML-код готовых блоков. Такая политика в общем то не удивительна, надо же как-то монетизировать такие продукты.

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

Итак пример структуры одноуровневого меню, которое будем превращать в двухуровневое:

<ul>
<li>First</li>
<li>Second</li>
<li>Third</li>
<li>Four</li>
<li>Five</li>
<li>Sub el 1</li>
<li>Sub el 2</li>
</ul>

И CSS:

ul {
  word-spacing: -.36em;
  display: table;
  position: relative;
  margin: 0;
  padding: 0;
}
li {
  display: inline-block;
  padding: 10px;
  word-spacing: normal;
  height: 20px;
  width: 80px;
  background: gray;
}

ul:hover li {
  display: inline-block;
}

li:nth-child(6),
li:nth-child(7) {
  display: none;
  position: absolute;
  left: 200px;
  background: lightgray;
}
li:nth-child(6) {
  top: 40px;
}
li:nth-child(7) {
  top: 80px;
}
li:nth-child(1):hover ~ li:nth-child(6),
li:nth-child(2):hover ~ li:nth-child(6),
li:nth-child(4):hover ~ li:nth-child(6),
li:nth-child(5):hover ~ li:nth-child(6),
li:nth-child(1):hover ~ li:nth-child(7),
li:nth-child(2):hover ~ li:nth-child(7),
li:nth-child(4):hover ~ li:nth-child(7),
li:nth-child(5):hover ~ li:nth-child(7) {
  display: none;
}

Вот что у нас получилось:

  • First
  • Second
  • Third
  • Four
  • Five
  • Sub el 1
  • Sub el 2

Как видно, выпадающие пункты меню размещены в конце, иначе начинаются проблемы при отображении. Некоторый CSS-код относится к решению проблемы отступов между элементами типа inline-block (В частности word-spacing и display: table у UL). Неудобство, конечно, еще в том что все отступы пунктов выпадающего меню придется прописывать вручную. А для разных разрешений экрана, по видимому, их надо будет настраивать с помощью медиа-запросов CSS.

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

Использование Git, если мало места на хостинге или нет доступа к SSH

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

Что если нужно вести разработку сайта, а к SSH доступа нет. Или, например, для хранения репозитория на рабочем сервере нет места. Конечно, речь идет не о крупных проектах. А скорее о небольших сайтах, где экономия иногда идет на всём и используется недорогой shared-хостинг. Повод ли это отказываться от Git? Не обязательно. Хотя конечно некоторые его преимущества будут потеряны.

Суть подхода проста. На локальном компьютере можно вести разработку как обычно, отличается лишь выгрузка на production (рабочий сервер). Обновления можно делать с помощью патчей по FTP. А для формирования патча можно использовать следующий код из консоли (где COMMITHASH — хеш коммита, начиная с которого делаем патч). Команды выполняются из рабочей директории сайта:

mkdir patch
cp --parents $(git diff --no-commit-id --name-only -r COMMITHASH) ./patch

После выполнения этих команд, в папке patch будет сформирован патч, который включает все измененные файлы от указанного коммита до HEAD, с сохранением структуры папок. Остаётся залить содержимое этой директории по FTP на сервер.

Папку patch, после применения патча, нужно удалить из рабочей копии или занести её в файл .gitignore, чтобы она не индексировалась.

Сложно представить такой подход в команде разработчиков, но если разработчиков всего два или три, достаточно использовать Git-сервер (или например приватный репозиторий на Bitbucket), через который будет идти обмен данными. А загрузку патчей на рабочий сайт по FTP может осуществлять, по согласованию, один человек.

Организация рабочего процесса с Trello или замена стикеров на монитор

На вашем мониторе приклеена куча стикеров? Ну или может они уже перекочевали в электронный вид на рабочий стол в виде виджетов? Тогда вам возможно пригодится web-сервис призванный организовать рабочие процессы или же что угодно, как гласит слоган на сайте Trello.com.

Кроме того сервис может то, что не могут обычные стикеры — можно вести динамические журналы задач совместно со своими коллегами или, например, с семьей. К тому же для мобильных устройств есть соответствующее приложение, с помощью которого вы можете получить доступ к своим задачам где угодно. Даже если использовать Trello обособлено, для себя, он вполне оправдан. Хотя конечно возлагать все на компьютеры — это еще один шаг к ухудшению собственной памяти. Но если количество задач слишком велико, а забыть что-то из них — для вас непозволительная роскошь, думаю использование сервиса оправдано и на личном уровне.

Структура приложения представляет собой набор произвольно названных досок, каждая из которых может содержать произвольные списки карточек-дел. Карточки можно перемещать между списками или досками. Сами карточки могут содержать фото, вложенные файлы, ссылки, комментарии и еще многое другое. Gold-версия еще больше расширяет возможности кастомизации списков и карточек, но даже обычной более чем достаточно для серьезной работы. Кстати, в текущей версии при создании новой доски в мобильном приложении для Android автоматически создаются три списка по умолчанию: «Нужно сделать», «В процессе» и «Готово» — это довольно удобно. А при создании доски через веб-интерфейс, списки по умолчанию почему-то отсутствуют. Ну, думаю это исправят.

организация чего угодно - Trello.com

Кстати доски бывают приватными, командными и публичными. Можно найти человека в Trello по имени или адресу электронной почты, а затем добавить в качестве участника в определенной доске — и ваши задачи будут синхронизированы. Если кто-то не зарегистрирован в сервисе, его можно пригласить с помощью ссылки (не в Сибирь конечно :)).

Ну и напоследок, так как этот сайт преимущественно про web-разработку, логично упомянуть, что Trello может быть хорошим инструментом для взаимодействия заказчика сайта и исполнителя. Можно вести список текущих задач, а так же отдельные списки по планируемым доработкам. Как уже упоминалось выше, к карточкам-задачам можно прикреплять скриншоты, ссылки на другие проекты, комментарии и так далее. А если разработку ведет команда, роль инструмента еще более возрастает. Какие то доски могут быть доступны и разработчикам и заказчику(ам), а какие-то чисто разработчику. Можно разграничить взаимодействие более узко — например участвуют только программисты или только дизайнеры. Вобщем простор достаточный.

Конечно Trello не единственный подобный сервис, однако он заслуживает рассмотрения. Особенно если учесть, что львиная доля функционала — бесплатна.