Writing Unit Tests for 1C-Bitrix Modules

Introduction On one logistics project, the delivery calculation module produced incorrect rates after each update due to a lack of unit tests. After refactoring and writing tests, the errors disappeared, and the time to implement changes halved. Regression costs dropped by 70%, and the support bu

Our competencies:

Frequently Asked Questions

Introduction

On one logistics project, the delivery calculation module produced incorrect rates after each update due to a lack of unit tests. After refactoring and writing tests, the errors disappeared, and the time to implement changes halved. Regression costs dropped by 70%, and the support budget for the module was cut in half. This is a typical scenario: Bitrix module code is often tightly coupled with the core, making it difficult to test. Our team's experience across dozens of projects confirms: proper architecture and unit tests cut new feature development time in half and reduce bugs by 70%. We regularly encounter projects where business logic is not isolated, and writing tests requires prior refactoring. However, even in such cases, unit tests pay off within the first months of operation.

Why Unit Tests in Bitrix Are Harder Than in Other Projects

Common problems we see at the start:

  • Tight coupling with the core. Classes inherit CBitrixComponent or call CIBlockElement directly. Any test requires initializing the entire environment.
  • Lack of interfaces. Repositories are often not extracted into separate classes. Instead, SQL queries are scattered across methods.
  • Global state. Bitrix uses global variables ($APPLICATION, $DB) and singletons. This breaks test isolation.
  • Slow bootstrap. Loading the core via prolog_before.php takes 1–3 seconds, making running a thousand tests unacceptably slow.

Because of these factors, many developers abandon unit testing. But we found an approach that makes it effective.

Principles of Testable Architecture

The first step is to extract business logic into separate classes with clear interfaces. Don't do this:

// Business logic mixed with infrastructure — cannot test 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; } 

Instead, inject dependencies:

// Logic separated, dependencies 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 Bitrix API in production code and via mocks in tests. This refactoring pays off after the first cycle of changes. More on dependency injection in Bitrix documentation.

Test Infrastructure

We set up PHPUnit with two bootstraps: one for pure unit tests (no kernel, only Composer autoload), the other for integration tests that load Bitrix.

Example bootstrap for integration tests:

// 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 isolated tests, a separate bootstrap without prolog_before.php. This speeds up the run by 10–50 times.

Example Unit Tests

Test business logic (without kernel):

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)); } } 

Integration tests with the kernel are used only to check ORM or complex call chains. They run separately, not in the main suite.

What ROI Do Unit Tests Deliver?

Implementing unit tests cuts debugging time by 70% and reduces maintenance costs by half due to early regression detection. On average, one business operation (calculation, validation) takes 2 to 6 hours to write tests. A full cycle for a medium-sized module is 2–5 days. Timelines depend on the current architecture and the need for refactoring. If you want to improve project stability, contact us for a testability audit.

Approach Comparison

Criteria Isolated unit tests Integration with kernel
Execution speed 0.01–0.1 sec 1–5 sec
Database dependency No Yes
Requires mocks Yes No
Covers business logic Yes Yes
Covers infrastructure No Yes
Debugging ease High Medium

Isolated tests are 50 times faster — we choose them for most scenarios.

Coverage and Prioritization

You don't need to cover 100% of the code. Priorities:

Priority What to test
High Price, discount, delivery cost calculations
High Business logic of states (state machine)
High Data parsers and mapping (import from 1C, Excel)
Medium Input validators
Medium Report generation algorithms
Low Component templates, UI logic

Target coverage for key business logic is 80%+. For infrastructure code (repositories, adapters), integration tests are sufficient. This balances speed and reliability.

What's Included in Writing Unit Tests

  1. Audit module code for testability, refactor dependency injection points.
  2. Set up PHPUnit with bootstrap for Bitrix environment.
  3. Write tests for business logic: calculations, state machines, parsers.
  4. Set up coverage report via Xdebug.
  5. Integrate test execution into CI (GitHub Actions / GitLab CI).
  6. Document how to run tests and add new ones.

We guarantee quality: our engineers have over 10 years of Bitrix development experience. Order turnkey unit test writing — get stable, easily maintainable code. We'll evaluate your project for free, just contact us.

Common Mistakes When Writing Tests for Bitrix

  • Trying to load the kernel for every test: use two bootstraps.
  • Using a real database in unit tests: mock repositories.
  • Ignoring result caching: clear cache between tests.
  • Testing protected methods via reflection: extract logic into public methods.