Writing unit 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

Writing Unit Tests for 1C-Bitrix Modules

Unit testing Bitrix modules is a doubly challenging task. The first challenge — module code is often written with direct calls to static core methods (CIBlockElement::GetList(), \Bitrix\Main\Application::getInstance()) that cannot be replaced with mocks. The second — tests require the Bitrix core to be loaded, which makes them slower than typical unit tests. The solution: a clear separation of testable logic from Bitrix infrastructure code.

Principle of Testable Architecture

Untestable code:

// Business logic mixed with infrastructure — cannot be tested in isolation
public function calculateDiscount(int $userId): float
{
    $user = \CUser::GetByID($userId)->Fetch(); // static Bitrix call
    $orders = \CSaleOrder::GetList([], ['USER_ID' => $userId])->Fetch();
    return $orders['count'] > 10 ? 0.15 : 0.05;
}

Testable code:

// Logic is separated, dependencies are injected
class DiscountCalculator
{
    public function __construct(
        private UserRepositoryInterface $users,
        private OrderRepositoryInterface $orders,
    ) {}

    public function calculate(int $userId): float
    {
        $user = $this->users->findById($userId);
        $orderCount = $this->orders->countByUserId($userId);
        return $orderCount > 10 ? 0.15 : 0.05;
    }
}

Repositories are implemented via the Bitrix API in production code and via mocks in tests.

Test Infrastructure

PHPUnit bootstrap with Bitrix core loading (when it cannot be avoided):

// tests/bootstrap.php
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_WITH_ON_AFTER_EPILOG', false);
define('BX_NO_ACCELERATOR_RESET', true);

$_SERVER['DOCUMENT_ROOT'] = realpath(__DIR__ . '/../../../..');
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
\Bitrix\Main\Loader::includeModule('your.module');

For pure unit tests without the Bitrix core — a separate bootstrap without prolog_before.php, using Composer autoload only.

Unit Test Examples

Business logic test (without core):

class DiscountCalculatorTest extends TestCase
{
    private DiscountCalculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new DiscountCalculator(
            users: $this->createStub(UserRepositoryInterface::class),
            orders: $this->createConfiguredMock(
                OrderRepositoryInterface::class,
                ['countByUserId' => 5]
            ),
        );
    }

    public function testLessThan10OrdersGivesBasicDiscount(): void
    {
        $this->assertSame(0.05, $this->calculator->calculate(1));
    }

    public function testMoreThan10OrdersGivesPremiumDiscount(): void
    {
        $repo = $this->createConfiguredMock(
            OrderRepositoryInterface::class,
            ['countByUserId' => 15]
        );
        $calc = new DiscountCalculator($this->createStub(UserRepositoryInterface::class), $repo);
        $this->assertSame(0.15, $calc->calculate(1));
    }
}

Test using the Bitrix ORM (integration):

class BookingTableTest extends \Bitrix\Main\Test\TableTest
{
    protected static function getTable(): string
    {
        return BookingTable::class;
    }

    public function testCreateBooking(): void
    {
        $result = BookingTable::add([
            'ROOM_ID' => 1,
            'DATE_FROM' => new \Bitrix\Main\Type\Date('2026-04-01'),
            'DATE_TO' => new \Bitrix\Main\Type\Date('2026-04-05'),
            'STATUS' => 'pending',
        ]);
        $this->assertTrue($result->isSuccess(), implode(', ', $result->getErrorMessages()));
        $this->assertGreaterThan(0, $result->getId());
    }
}

Coverage and Prioritization

100% code coverage is not necessary — it is not cost-effective for Bitrix projects. Priorities:

Priority What to test
High Price, discount, and shipping cost calculations
High Business logic for state transitions (state machines)
High Parsers and data mapping (1C, Excel imports)
Medium Input data validators
Medium Report generation algorithms
Low Component templates, UI logic

Coverage Metrics

Enabling Xdebug to measure coverage:

<!-- phpunit.xml -->
<coverage processUncoveredFiles="true">
    <include>
        <directory suffix=".php">local/modules/your.module/lib</directory>
    </include>
    <report>
        <html outputDirectory="coverage-report"/>
    </report>
</coverage>

Target coverage for core business logic — 80%+. For infrastructure code (repositories, adapters) — integration tests are sufficient.

What Is Included in Unit Test Writing

  • Module code audit for testability, refactoring of dependency injection points
  • PHPUnit setup with bootstrap for the Bitrix environment
  • Writing tests for business logic: calculations, state machines, parsers
  • Coverage report setup via Xdebug
  • Integration of test execution into CI (GitHub Actions / GitLab CI)
  • Documentation on running tests and adding new ones