Setting up PHPUnit tests for 1C-Bitrix modules

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1177
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Setting Up PHPUnit Tests for 1C-Bitrix Modules

A 1C-Bitrix module without tests is a black box. Fix one thing, break another. This is especially painful in modules containing business logic: discount calculations, third-party API integrations, order processing. PHPUnit for Bitrix modules has its own specifics: the Bitrix core must be bootstrapped (which is slow), static calls hinder isolation, and Bitrix itself provides its own ORM testing utilities.

Setting Up PHPUnit Tests for 1C-Bitrix Modules

Test Structure in a Bitrix Module

/local/modules/vendor.mymodule/
    lib/
        Services/
            DiscountService.php
            ShippingCalculator.php
        Repository/
            OrderRepository.php
    tests/
        bootstrap.php
        Unit/
            Services/
                DiscountServiceTest.php
                ShippingCalculatorTest.php
        Integration/
            Repository/
                OrderRepositoryTest.php
    phpunit.xml
    composer.json

phpunit.xml for a Bitrix Module

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
    bootstrap="tests/bootstrap.php"
    cacheDirectory=".phpunit.cache"
    executionOrder="depends,defects"
    requireCoverageMetadata="false"
    beStrictAboutCoverageMetadata="false"
>
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory suffix=".php">lib</directory>
        </include>
    </source>

    <coverage>
        <report>
            <html outputDirectory="tests/_coverage"/>
            <clover outputFile="tests/_coverage/clover.xml"/>
        </report>
    </coverage>
</phpunit>

bootstrap.php: Two Modes

<?php
// tests/bootstrap.php

$bitrixLoaded = false;

// Unit tests without the Bitrix core — fast
if (getenv('PHPUNIT_NO_BITRIX') === 'true') {
    require_once __DIR__ . '/../vendor/autoload.php';
    return;
}

// Integration tests with the Bitrix core — slower
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_WITH_ON_AFTER_EPILOG', false);
define('BX_NO_ACCELERATOR_RESET', true);
define('STOP_STATISTICS', true);

$docRoot = realpath(__DIR__ . '/../../../..');
$_SERVER['DOCUMENT_ROOT'] = $docRoot;
$_SERVER['HTTP_HOST']     = 'localhost';
$_SERVER['SERVER_NAME']   = 'localhost';

require_once $docRoot . '/bitrix/modules/main/include/prolog_before.php';
require_once __DIR__ . '/../vendor/autoload.php';

\Bitrix\Main\Loader::includeModule('vendor.mymodule');

Unit Test with Isolated Business Logic

// tests/Unit/Services/DiscountServiceTest.php
namespace Tests\Unit\Services;

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Vendor\Mymodule\Services\DiscountService;
use Vendor\Mymodule\Repository\OrderRepositoryInterface;
use Vendor\Mymodule\Repository\UserRepositoryInterface;

class DiscountServiceTest extends TestCase
{
    private DiscountService $service;
    private OrderRepositoryInterface&MockObject $orders;
    private UserRepositoryInterface&MockObject $users;

    protected function setUp(): void
    {
        $this->orders  = $this->createMock(OrderRepositoryInterface::class);
        $this->users   = $this->createMock(UserRepositoryInterface::class);
        $this->service = new DiscountService($this->orders, $this->users);
    }

    public function testNewUserGetsNoDiscount(): void
    {
        $this->orders->method('countCompletedByUser')->willReturn(0);
        $this->users->method('getRegistrationDays')->willReturn(5);

        $discount = $this->service->calculate(userId: 1, orderAmount: 5000.0);

        $this->assertSame(0.0, $discount);
    }

    public function testUserWith5OrdersGets5PercentDiscount(): void
    {
        $this->orders->method('countCompletedByUser')->willReturn(5);
        $this->users->method('getRegistrationDays')->willReturn(180);

        $discount = $this->service->calculate(userId: 1, orderAmount: 5000.0);

        $this->assertSame(250.0, $discount); // 5% of 5000
    }

    public function testDiscountCappedAt20Percent(): void
    {
        $this->orders->method('countCompletedByUser')->willReturn(100);
        $this->users->method('getRegistrationDays')->willReturn(1000);

        $discount = $this->service->calculate(userId: 1, orderAmount: 10000.0);

        $this->assertSame(2000.0, $discount); // 20% — maximum
    }
}

Integration Test with Bitrix ORM

// tests/Integration/Repository/OrderRepositoryTest.php
namespace Tests\Integration\Repository;

use PHPUnit\Framework\TestCase;
use Vendor\Mymodule\Repository\OrderRepository;

class OrderRepositoryTest extends TestCase
{
    private static int $testUserId;

    public static function setUpBeforeClass(): void
    {
        // Create a test user
        $user = new \CUser();
        self::$testUserId = $user->Add([
            'LOGIN'      => 'test_' . uniqid(),
            'PASSWORD'   => 'Test123!',
            'EMAIL'      => 'test_' . uniqid() . '@test.ru',
            'ACTIVE'     => 'Y',
            'GROUP_ID'   => [2],
        ]);
    }

    public static function tearDownAfterClass(): void
    {
        // Remove test data
        \CUser::Delete(self::$testUserId);
    }

    public function testCountCompletedOrdersReturnsCorrectNumber(): void
    {
        $repo = new OrderRepository();

        // Create test orders via Sale API
        $this->createTestOrder(self::$testUserId, 'F'); // completed
        $this->createTestOrder(self::$testUserId, 'F');
        $this->createTestOrder(self::$testUserId, 'N'); // new, not counted

        $count = $repo->countCompletedByUser(self::$testUserId);
        $this->assertSame(2, $count);
    }

    private function createTestOrder(int $userId, string $status): void
    {
        \Bitrix\Main\Loader::includeModule('sale');

        $order = \Bitrix\Sale\Order::create('s1', $userId);
        $order->setField('STATUS_ID', $status);
        $order->setField('CURRENCY', 'RUB');
        $order->setField('PRICE', 1000.0);
        $order->save();
    }
}

Running Only Unit Tests (Without Loading Bitrix)

# Fast unit tests without the Bitrix core (seconds)
PHPUNIT_NO_BITRIX=true vendor/bin/phpunit --testsuite Unit

# Integration tests with the core (minutes)
vendor/bin/phpunit --testsuite Integration

# All tests with coverage (requires Xdebug or PCOV)
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html tests/_coverage

CI/CD Configuration

# .github/workflows/tests.yml
- name: Run PHPUnit Unit tests
  run: |
    cd local/modules/vendor.mymodule
    PHPUNIT_NO_BITRIX=true vendor/bin/phpunit --testsuite Unit
  env:
    PHPUNIT_NO_BITRIX: 'true'

Timelines

Task Timeline
Set up PHPUnit, bootstrap, module configuration 4–8 hours
Unit tests for module business logic (up to 10 classes) 1–2 days
Integration tests with Bitrix ORM 1–2 days
Refactor module for testability + 70%+ coverage 3–7 days