Testing in 1C Bitrix


Foreword

Speaking about the development of sites using CMS 1C Bitrix, the issue of test coverage is rarely raised. The main reason is that most projects get by with the regular functionality provided by the system – it is difficult (and, in general, there is no need) to test it.

But over time, the project grows, it becomes necessary to integrate with third-party services and services (payment systems, delivery service APIs, and others), or more and more specialized functionality is being developed. And the further, the greater the amount of code, control over which lies with the developer.
This is the prerequisite for introducing a testing mechanism into the CMS.

The process of preparing the environment for writing tests consists of several steps:

  1. install Composer;

  2. configuring Bitrix to work with Composer;

  3. install PHPUnit;

  4. configure PHPUnit to work with Bitrix.

Composer

Installation

We install composer by instructions.

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;"

Upon completion, we see a message in the console that the installer was downloaded successfully:

Installer verified

Let’s move on to the installation:

sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer

At the end, we see a message about the successful installation:

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

We organize all work with dependencies in the directory local. We initialize the project:

cd local
composer init

Specify the required parameters, confirm.
Upon completion, we have a /local/composer.json file with something like this:

{
    "name": "myproject/website",
    "type": "project",
    "authors": [
        {
            "name": "Andriy Kryvenko",
            "email": "krivenko.a.b@gmail.com"
        }
    ]
}

Now let’s tell Bitrix that we need to use third-party packages installed via Composer.
Opening the file /local/php_interface/init.php (create if it doesn’t exist) and include the autoload file:

<?php

include_once(__DIR__.'/../vendor/autoload.php');

After that, we hide the /local/vendor/ directory from the version control system. Add /local/vendor/* to the .gitignore file

PHPUnit

Let’s move on to installing PHPUnit. We don’t need it on the production server, so we install it only as a dev dependency and create a config file. To do this, run on the command line:

composer require --dev phpunit/phpunit ^9.0
./vendor/bin/phpunit --generate-configuration

phpunit was added to the dev dependency, and the /local/phpunit.xml file was also created
In addition to PHPUnit itself, for a nicer view of test results, I use the package sempro/phpunit-pretty-print.

composer require --dev sempro/phpunit-pretty-print ^1.4

Now we need to create a file that will be used during testing to initialize the core of the product. Let’s call it /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");

Let’s set up PHPUnit to use our initialization file and our result decoration class. Let’s open the file /local/phpunit.xml and bring it to the following form:

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>

And add a command to quickly run tests

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"
    }
}

Now when running the command

composer test

all tests will run. This completes the configuration process and you can proceed to writing tests.

Before proceeding

For convenience, it is better to place your classes in the local/classes directory, approximately in the following form:

/local/classes/MyProject/Product.php
/local/classes/MyProject/Rests.php

And so that there is no need to connect them manually every time – we register in the file /local/php_interface/init.php loader that will include these classes automatically as needed:

<?php

spl_autoload_register(function($className) {
    $className = str_replace("\\", DIRECTORY_SEPARATOR, $className);
    include_once $_SERVER['DOCUMENT_ROOT'] . '/local/classes/' . $className . '.php';
});

include_once(__DIR__.'/../vendor/autoload.php');

Test Example

As an example, I will show the real situation, its solution and the tests that cover this solution (I do not include the part of the code that is not directly related to transformations).

Actually, the situation: from 1C to the site in the form of strings, information about the available delivery times of the goods is transmitted, for example:

24 часа-7|до 2 дней-14|до 15 дней-неогр

When buying up to 7 pieces – we will deliver in 24 hours, up to 14 pieces – in 2 days, otherwise – in 15 days.

Need to convert them to objects left over for future use. The transformation is done using 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;
    }
}

And we cover these classes with the corresponding tests:

left over test
<?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
            ]
        ];
    }
}

The above test example allows us to be sure that when working with goods, we always know exactly whether a particular quantity of a product is available for purchase and in what period.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *