Наш блог
Показать рубрики

Пример написания юнит-тестов в 1С-Битрикс проекте

Назад к списку статей
Пример написания юнит-тестов в 1С-Битрикс проекте
В этом посте будут рассмотрены примеры юнит-тестов, которые проверяют работоспособность класса по реализации виртуальных групп в магазине на 1С-Битрикс. Я опишу работу функционала виртуальных групп, а также подробно разберу код PHPUnit-тестов для его тестирования.

Что такое PHPUnit

PHPUnit - это фреймворк для модульного тестирования в PHP проектах. Он есть представителем семейства фреймворков xUnit на основе пакета sUnit, сознанного Кентом Беком. PHPUnit разработан немецким программистом Себастьяном Бергманом.

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

PHPUnit предоставляет программисту следующие возможности:

  • Инструменты для создания модульных тестов и организации их в иерархические наборы.
  • Интерфейс командной строки для тестирования
  • Поставщики данных - генераторы для тестирования данных для проверки, как единственный тест ведет себя на разных входных данных.
  • Поддержка тестирования кода, использующего базу данных.
  • Возможность тестирования исключений.
  • Поддержка так называемых фиктивных объектов
  • Генератор отчетов
  • Интеграция с инструментом Selenium RC для тестирования пользовательских интерфейсов

PHPUnit без проблем позволяет тестировать сайты на базе Bitrix Framework. Перед тем, как продолжить чтение, рекомендую ознакомиться с моей статьей, где описано как можно подружить 1С-Битрикс и PHPUnit.

Какой функционал будем тестировать

Допустим, у нас стоит задача покрыть тестами функционал виртуальных групп, который реализован в виде класса. Понятие виртуальных групп подробно рассмотрено в нашей статье - Виртуальные группы в интернет-магазине на 1С-Битрикс. Если кратко - мы заводим в каталоге разделы, в котором "физически" не привязаны товары. Мы можем наполнять эти виртуальные разделы с помощью заполнения специальных свойств, по которым строится фильтр и товары появляются в нем на лету. Это дает много преимуществ и позволяет гибко добавлять новые разделы для продвижения магазина при этом сохраняя основную структуру вашего каталога.

Перейдем к техническим деталям. Функционал довольно сложный и работа его зависит от многих параметров, которые сложно проверять вручную при возникновении ошибки. Авто тесты позволяют решить эту проблему и быстро покажут в каком месте кода происходит неладное. Функционал работает на базе пользовательских свойств разделов, в которые мы набиваем правила определения товаров, входящих в текущую виртуальную группу. Также у нас есть методы, которые извлекают эти свойства и строят фильтр для метода CIblockElement::GetList(), который получает товары, соответствующие заданным правилам. В виртуальный раздел можно собирать товары по двум типам настроек: по привязке к другим разделам в инфоблоке и по привязке по свойствам товара. При этом, привязка по разделам и свойствам множественная и можно указывать логику (И/ИЛИ) для соотношения разделов и элементов между собой соответственно. По этому есть отдельные методы, которые получают фильтры по этим типам настроек.

Разбираем примеры юнит-тестов

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


    public function testModuleInstalled()
    {
        $this->assertTrue(Loader::includeModule("iblock"));
    }

Далее нужно проверить все ли нужные пользовательские свойства есть в системе. Это можно сделать в тесте сделав выборку по заданным символьным кодам свойств используя API 1С-Битрикс:


    public function testCatalogSectionHasNeededUserProps()
    {
        $arUserFields = array();
        $arUserFieldsCodes = array(
            "UF_ELEMENTS_PROPS",
            "UF_LOGIC_ELEMENTS",
            "UF_VIRTUAL_SECTIONS",
            "UF_LOGIC_SECTION",
            "UF_IS_VIRTUAL_GROUP",
            "UF_NAMES_TO_VG"
        );
        $rsData = \CUserTypeEntity::GetList(
            array("ID" => "DESC"),
            array("FIELD_NAME" => $arUserFieldsCodes)
        );

        while($arRes = $rsData->Fetch())
        {
            if(in_array($arRes["FIELD_NAME"], $arUserFieldsCodes))
            {
                $arUserFields[] = $arRes["FIELD_NAME"];
            }
        }

        foreach($arUserFieldsCodes as $arUserFieldsCode)
        {
            $this->assertTrue(
                in_array($arUserFieldsCode, $arUserFields),
                "В созданы не все нужные пользовательские свойства! (" . $arUserFieldsCode . ")"
            );
        }
    }

Этот тест есть фундаментальным, по этому от него зависят несколько других тестов, которые я опишу ниже. Т.е. они должны выполняться только при условии, что testCatalogSectionHasNeededUserProps() прошел успешно, иначе нет смысла их проверять.

Следующие 2 теста проверяют правильно ли работают методы по извлечению идентификаторов флага И/ИЛИ для разделов и свойств элементов (нужно для внутренней логики функционала). Тестируемые методы вызываются в конструкторе и сохраняют идентификаторы значений пользовательских свойств в свойства объекта. Значит задача тестов вручную получить правильные идентификаторы значений пользовательских свойств, взять их за эталон, создать экземпляр тестируемого класса и проверить нужные свойства объекта на соответствие эталону. Вот как это выглядит:


    /**
     * @depends testCatalogSectionHasNeededUserProps
     */
    public function testLogicAndForElementHasNeededID()
    {
        $elementAndEnumValueId = 0;
        $rsData = \CUserTypeEntity::GetList(
            array("ID" => "DESC"),
            array("FIELD_NAME" => "UF_LOGIC_SECTION")
        );

        if($arRes = $rsData->Fetch())
        {
            $rsLogicAndProp = \CUserFieldEnum::GetList(
                array(),
                array("USER_FIELD_ID" => $arRes["ID"], "VALUE" => "AND")
            );
            if($arLogicAndProp = $rsLogicAndProp->GetNext())
            {
                $elementAndEnumValueId = $arLogicAndProp["ID"];
            }
        }

        $obVG = new VirtualGroups("test_section");

        $this->assertEquals(
            $obVG::$LOGIC_AND_SECTION_PROP_ID,
            $elementAndEnumValueId,
            "ID значения AND для фильтра елементов в виртуальной группе не равно ожидаемому
            ID в классе \\Epages\\VirtualGroups"
        );
    }

    /**
     * @depends testCatalogSectionHasNeededUserProps
     */
    public function testLogicAndForSectionHasNeededID()
    {
        $elementAndEnumValueId = 0;
        $rsData = \CUserTypeEntity::GetList(
            array("ID" => "DESC"),
            array("FIELD_NAME" => "UF_LOGIC_ELEMENTS")
        );

        if($arRes = $rsData->Fetch())
        {
            $rsLogicAndProp = \CUserFieldEnum::GetList(
                array(),
                array("USER_FIELD_ID" => $arRes["ID"], "VALUE" => "AND")
            );
            if($arLogicAndProp = $rsLogicAndProp->GetNext())
            {
                $elementAndEnumValueId = $arLogicAndProp["ID"];
            }
        }

        $obVG = new VirtualGroups("test_section");

        $this->assertEquals(
            $obVG::$LOGIC_AND_ELEMENT_PROP_ID,
            $elementAndEnumValueId,
            "ID значения AND для фильтра елементов в виртуальной группе не равно ожидаемому
            ID в классе \\Epages\\VirtualGroups"
        );
    }

Обратите внимание на аннотацию методов - @depends testCatalogSectionHasNeededUserProps. Это аннотация предоставляемая PHPUnit и она устанавливает правило, что конкретный тест должен выполняться только в том случае, если заданный в зависимости тест прошел успешно.

Далее нам нужно проверить как себя ведет конструктов если передать ему неверный входной параметр. В нашем случае он должен выбросить исключение базового класса исключений - Exception. Это можно проверить с помощью встроенной аннотации @expectedException, вот так:


    /**
     * @expectedException \Exception
     */
    public function testThrowsExceptionOnEmptySectionCode()
    {
        new VirtualGroups("");
    }

Следующий шаг - проверка методов, которые строят фильтр и возвращают его клиентскому коду. Для проведения этих тестов нам нужно воспользоваться мокком объекта тестируемого класса. Мокк (англ. Mock) - это специальный фиктивный объект, который позволяет в нашем случае сымитировать получение данных по разделу из внешнего источника. Наш тест не должен зависеть от информации конкретного набора виртуальных разделов в БД, по этому мы подменяем (имитируем) реальное извлечение массива из БД на свой статический заданный прямо в коде массив.

Так как этот фиктивный mock-объект будет нужен нам в нескольких тестах - мы вынесем его в отдельных protected-метод. PHPUnit при выполнении тестов не будет считать этот метод тестом и не будет пытаться его выполнить. Вот код этого метода:


    protected function getVirtualGroupMock($arMethods = array())
    {
        $mock = $this->getMockBuilder('\\Epages\\VirtualGroups')
                     ->setConstructorArgs(array("test_section"))
                     ->setMethods($arMethods)
                     ->getMock();

        $mock->method('getGroup')
             ->willReturn(
                 array(
                     "NAME" => "Виртуальная группа",
                     "UF_VIRTUAL_SECTIONS" => array(1412),
                     "UF_ELEMENTS_PROPS" => array(1411),
                     "UF_LOGIC_ELEMENTS" => 5,
                     "UF_LOGIC_SECTION" => 3,
                     "UF_IS_VIRTUAL_GROUP" => "1"
                 )
             );
        /**
         * @var VirtualGroups $mock
         */
        $mock->setGroupProp("test_section");

        return $mock;
    }

Следующий тест с использование mock-объекта проверяет работу метода, определяющего какой раздел был передан конструктору на вход: реальный или виртуальный:


    public function testIsVirtualGroup()
    {
        $stub = $this->getVirtualGroupMock(array("getGroup"));

        $stub->setGroupProp("test_section");

        $this->assertTrue($stub->isVirtualGroup(), "Ошибка определения раздела как виртуальной группы!");
    }

Чтоб протестировать работу метода, который строит фильтр, нам нужно проверить, что он внутри себя хотяб один раз вызывает методы по постройке фильтра по частям (привязка к разделам и свойства элементов):


    public function testGetVirtualFilter()
    {
        $mock = $this->getVirtualGroupMock(array("getGroup", "decodeElementProps", "getSectionsFilter", "getElementsFilter"));

        $mock->expects($this->atLeast(1))
            ->method("getSectionsFilter");

        $mock->expects($this->atLeast(1))
            ->method("getElementsFilter");

        $mock->getVirtualFilter(Array());
    }

Также нам критически важно, чтоб результирующий массив с фильтром возвращал ключ VIRTUAL_GROUP, так как его использует клиентский код. Вот тест, который проверяет этот момент:


    public function testVirtualGroupFilterContainsNeededKey()
    {
        $mock = $this->getVirtualGroupMock(array("getGroup"));

        $arFilter = $mock->getVirtualFilter(array());

        $this->assertEquals(
            true,
            $arFilter["VIRTUAL_GROUP"],
            "Результирующий фильтр должен содержать ключ VIRTUAL_GROUP равный true"
        );
    }

Вот еще парочка тестов, которые проверяют внутренние методы тестируемого класса. Какое поведение они проверяют можно понят из assert-методов:


    public function testGetLinkedSectionsIds()
    {
        $mock = $this->getVirtualGroupMock(array("getGroup"));

        $this->assertTrue(
            count($mock->getLinkedSectionsIds()) > 0,
            "Ошибка получения привязанных разделов виртуальной группы в getLinkedSectionsIds()"
        );
    }

    public function testGetFilteredLinkedSections()
    {
        $mock = $this->getVirtualGroupMock(array("getGroup"));

        $this->assertFalse(
            $mock->getFilteredLinkedSections(array()),
            "Метод должен возвращать false если ему передали массив без нужных параметров"
        );
    }

Выводы

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

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

Полный код тест-класса


namespace Epages\Tests;

use \Bitrix\Main\Loader;
use \Epages\VirtualGroups;

class VirtualGroupsTest extends \PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        initBitrixCore();
    }

    public function testModuleInstalled()
    {
        $this->assertTrue(Loader::includeModule("iblock"));
    }

    public function testCatalogSectionHasNeededUserProps()
    {
        $arUserFields = array();
        $arUserFieldsCodes = array(
            "UF_ELEMENTS_PROPS",
            "UF_LOGIC_ELEMENTS",
            "UF_VIRTUAL_SECTIONS",
            "UF_LOGIC_SECTION",
            "UF_IS_VIRTUAL_GROUP",
            "UF_NAMES_TO_VG"
        );
        $rsData = \CUserTypeEntity::GetList(
            array("ID" => "DESC"),
            array("FIELD_NAME" => $arUserFieldsCodes)
        );

        while($arRes = $rsData->Fetch())
        {
            if(in_array($arRes["FIELD_NAME"], $arUserFieldsCodes))
            {
                $arUserFields[] = $arRes["FIELD_NAME"];
            }
        }

        foreach($arUserFieldsCodes as $arUserFieldsCode)
        {
            $this->assertTrue(in_array($arUserFieldsCode, $arUserFields), "В созданы не все нужные пользовательские свойства! (" . $arUserFieldsCode . ")");
        }
    }

    /**
     * @depends testCatalogSectionHasNeededUserProps
     */
    public function testLogicAndForElementHasNeededID()
    {
        $elementAndEnumValueId = 0;
        $rsData = \CUserTypeEntity::GetList(
            array("ID" => "DESC"),
            array("FIELD_NAME" => "UF_LOGIC_SECTION")
        );

        if($arRes = $rsData->Fetch())
        {
            $rsLogicAndProp = \CUserFieldEnum::GetList(
                array(),
                array("USER_FIELD_ID" => $arRes["ID"], "VALUE" => "AND")
            );
            if($arLogicAndProp = $rsLogicAndProp->GetNext())
            {
                $elementAndEnumValueId = $arLogicAndProp["ID"];
            }
        }

        $obVG = new VirtualGroups("test_section");

        $this->assertEquals(
            $obVG::$LOGIC_AND_SECTION_PROP_ID,
            $elementAndEnumValueId,
            "ID значения AND для фильтра елементов в виртуальной группе не равно ожидаемому
            ID в классе \\Epages\\VirtualGroups"
        );
    }

    /**
     * @depends testCatalogSectionHasNeededUserProps
     */
    public function testLogicAndForSectionHasNeededID()
    {
        $elementAndEnumValueId = 0;
        $rsData = \CUserTypeEntity::GetList(
            array("ID" => "DESC"),
            array("FIELD_NAME" => "UF_LOGIC_ELEMENTS")
        );

        if($arRes = $rsData->Fetch())
        {
            $rsLogicAndProp = \CUserFieldEnum::GetList(
                array(),
                array("USER_FIELD_ID" => $arRes["ID"], "VALUE" => "AND")
            );
            if($arLogicAndProp = $rsLogicAndProp->GetNext())
            {
                $elementAndEnumValueId = $arLogicAndProp["ID"];
            }
        }

        $obVG = new VirtualGroups("test_section");

        $this->assertEquals(
            $obVG::$LOGIC_AND_ELEMENT_PROP_ID,
            $elementAndEnumValueId,
            "ID значения AND для фильтра елементов в виртуальной группе не равно ожидаемому
            ID в классе \\Epages\\VirtualGroups"
        );
    }

    /**
     * @expectedException \Exception
     */
    public function testThrowsExceptionOnEmptySectionCode()
    {
        new VirtualGroups("");
    }

    protected function getVirtualGroupMock($arMethods = array())
    {
        $mock = $this->getMockBuilder('\\Epages\\VirtualGroups')
                     ->setConstructorArgs(array("test_section"))
                     ->setMethods($arMethods)
                     ->getMock();

        $mock->method('getGroup')
             ->willReturn(
                 array(
                     "NAME" => "Виртуальная группа",
                     "UF_VIRTUAL_SECTIONS" => array(1412),
                     "UF_ELEMENTS_PROPS" => array(1411),
                     "UF_LOGIC_ELEMENTS" => 5,
                     "UF_LOGIC_SECTION" => 3,
                     "UF_IS_VIRTUAL_GROUP" => "1"
                 )
             );
        /**
         * @var VirtualGroups $mock
         */
        $mock->setGroupProp("test_section");

        return $mock;
    }

    public function testIsVirtualGroup()
    {
        $stub = $this->getVirtualGroupMock(array("getGroup"));

        $stub->setGroupProp("test_section");

        $this->assertTrue($stub->isVirtualGroup(), "Ошибка определения раздела как виртуальной группы!");
    }

    public function testGetVirtualFilter()
    {
        $mock = $this->getVirtualGroupMock(array("getGroup", "decodeElementProps", "getSectionsFilter", "getElementsFilter"));

        $mock->expects($this->atLeast(1))
            ->method("getSectionsFilter");

        $mock->expects($this->atLeast(1))
            ->method("getElementsFilter");

        $mock->getVirtualFilter(Array());
    }

    public function testVirtualGroupFilterContainsNeededKey()
    {
        $mock = $this->getVirtualGroupMock(array("getGroup"));

        $arFilter = $mock->getVirtualFilter(array());

        $this->assertEquals(
            true,
            $arFilter["VIRTUAL_GROUP"],
            "Результирующий фильтр должен содержать ключ VIRTUAL_GROUP равный true"
        );
    }

    public function testGetLinkedSectionsIds()
    {
        $mock = $this->getVirtualGroupMock(array("getGroup"));

        $this->assertTrue(
            count($mock->getLinkedSectionsIds()) > 0,
            "Ошибка получения привязанных разделов виртуальной группы в getLinkedSectionsIds()"
        );
    }

    public function testGetFilteredLinkedSections()
    {
        $mock = $this->getVirtualGroupMock(array("getGroup"));

        $this->assertFalse(
            $mock->getFilteredLinkedSections(array()),
            "Метод должен возвращать false если ему передали массив без нужных параметров"
        );
    }
}

Назад к списку статей
Подпишись на наш блог:
ПОХОЖИЕ СТАТЬИ:
Виртуальные группы в интернет-магазине на 1С-Битрикс

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

Как автотесты упрощают жизнь. Selenium IDE - что это, как установить и использовать

В этом посте мы расскажем о интересном и полезном инструменте Selenium IDE, почему мы его используем, какие проблемы он призван решать, и покажем вам как использовать Selenium IDE уже сегодня.