Setting Up PHPUnit Tests for 1C-Bitrix Modules: From Bootstrap to CI/CD

Updating the Bitrix core often breaks custom modules, and regressions surface in production. We set up PHPUnit tests for Bitrix modules turnkey: from environment to automated runs. This cuts revision verification time dramatically and removes the fear of updates.

Our competencies:

Frequently Asked Questions

Why Bitrix modules break when the core is updated

A core update is the most common trigger for a custom module failing. The core changes the behavior of base classes, the order in which event handlers are connected, and the signatures of methods for working with infoblocks and cache.

A module that worked "by the fact" for years crashes on the very first call after an update: an event handler receives arguments in a different composition, the infoblock cache returns an outdated structure, and an agent runs before dependencies are initialized.

Without phpunit bitrix test coverage, a module breaks silently — in production, for the user, at the worst possible moment.

The second reason is a change of environment. Locally, the module ran on one PHP version and one cache configuration; in production, there is a different interpreter branch, compression enabled, and a different file handling mode.

Testing Bitrix modules without isolation from the environment does not catch these discrepancies: the code accesses global variables, the real database, and the live core — and the result depends on where it is run, not on the logic of the module itself.

The third reason is module regression after edits. When a module is modified for a new task, no one checks the old scenarios: an edit in a handler breaks an adjacent branch of logic, and this is discovered weeks later.

The module failing at that moment looks like "it suddenly stopped working," although the cause is an unnoticed regression.

All three scenarios are covered by one thing: the module must be checked automatically on every change — both of the core and of its own code. Let's break down how this works in practice.

What test coverage delivers: measurable effects

Module test coverage is not a "checkbox in a report" but a tool that changes the economics of releases.

When every module has a set of PHPUnit checks, regression testing stops being a half-day manual task: a run takes minutes rather than hours and is triggered before the code reaches production.

The main measurable effect is the share of regressions caught before deployment. A module with coverage of key scenarios cuts off most breakages at the build stage: the test fails, the release does not go out, there is no incident.

What remains is a predictable flow of fixes instead of fire drills of "the site is down, we're looking for the cause."

The second effect is release speed. When verification is automated, the team ships changes more often and in smaller chunks rather than accumulating a large batch that is scary to touch.

Small releases are easier to roll back, and recovery time after a failure drops: reverting to the previous version is a matter of minutes, not a hunt for the guilty commit.

The third is module stability during enhancements. A new developer stepping into someone else's code sees not only the implementation but also the fixed behavior: the tests describe what the module must do. This reduces the risk that a change in one place breaks logic in another.

Finally, test coverage makes effort estimation more honest. It is clear which scenarios are already protected and which rely on manual verification — and where the bottleneck is.

Next, we break down how this works at the level of a 1C-Bitrix module: from bootstrap to running in the build pipeline.

Test Environment Organization Options

The choice of environment determines how fast a test run passes and how reproducible it is between developers' machines and the build.

For modules under 1C-Bitrix, it is important to decide in advance where the product core lives, where the database lives, and how the test bootstrap is brought up before the first assert.

Option When it fits Limitations
Local stand One developer, quick debugging of individual tests Configuration drift between machines, manual database setup, "it works on my machine"
Containerization Teamwork, reproducibility, local runs and CI on the same image Need a shared image with the core and extensions, time to build layers
Dedicated server Heavy integration scenarios, stable data between runs Idle time between runs, manual updating of the core and dependencies
Cloud environment Parallel runs, ephemeral environments per branch Dependency on the network, delays on startup, harder debugging of failures

A local stand is good at the start: a developer edits a module and immediately runs a single test.

But as soon as there are dozens of tests and more than two people on the team, discrepancies in PHP versions and extensions start breaking runs — and time goes into sorting out the environment rather than the code.

Containerization removes this class of problems: the image pins the core, extensions, and test database, and the test bootstrap comes up identically for everyone and in CI.

A dedicated server and the cloud cover the remaining scenarios — heavy integrations and parallel runs per branch, where isolation matters more than startup speed. In practice, they are most often combined: a container as the base environment, the cloud for the run matrix.

Stages of implementing module testing

Implementing testing is not "install PHPUnit and write a couple of checks," but a sequence of steps where each subsequent one builds on the result of the previous one.

Below is how this looks on projects with 1C-Bitrix modules, including those already running in production and unable to stop during the work.

How the process is structured

  1. Audit of modules and failure points. We look at what the project consists of: custom modules, core customizations, event handlers, agents, components. We note what can be tested in isolation at all, and what is tied to a live information block or an external service. The output is a map of modules with priorities: where an error costs the most.
  2. Defining testing boundaries. We agree on what we cover with unit tests (calculation logic, parsing, access rights) and what with integration tests (working with the database, exchange with 1C, REST). Without this step, tests either duplicate each other or fail to catch real failures.
  3. Setting up PHPUnit and bootstrap. We bring up the environment: configuration, autoloading of the module's classes, substituting the Bitrix core with stubs where the database is not needed. Tests start running locally with a single command — this is the foundation for everything that follows.
  4. Writing the first automated tests. We start with critical modules, not with everything at once. The first checks should catch regressions that have already occurred — this way the team quickly sees the benefit and does not abandon the initiative.
  5. Running automated tests in CI. We connect the run to the build pipeline: tests run on every commit to the branch, and a failure blocks the merge. Separately, we set up a coverage report so we can see which modules are not yet protected.
  6. Stabilization and triage of failures. In the first weeks, tests are noisy: false positives, dependence on run order, unstable data. We clean this up until a red run means a real problem.
  7. Handing over documentation and regulations. We give the client's team a description of how to run, the rules for adding new tests, and an analysis of typical errors. After that, the developers maintain coverage themselves, without our ongoing support.

The order matters: if you start with CI without understanding the boundaries, the pipeline will fail at every step and will quickly be disabled. If you start with tests without setting up the environment, they will not run for other developers.

Describe your modules and the current build process — we will come back with an estimate and a plan by stages.

Artifacts: configurations, schemas, regulations

After project delivery, you're left not only with working tests but also with a complete set of artifacts — so the team can maintain and extend coverage without us. Everything is placed in the module repository and handed over along with access.

  • PHPUnit configuration — a working phpunit.xml with test suites split out (unit, integration), configured bootstrap file paths, and rules for excluding service directories.
  • Bootstrap initialization file — connects the Bitrix core, module autoloading, and test stubs so tests run identically on any machine and in the build pipeline.
  • Module dependency schema — a map of relationships between classes, services, and external components, marking what gets isolated in tests and what requires a real environment.
  • Access policies — regulations on permissions to run tests, read logs, and modify configuration: who on the team can touch what without approval.
  • Module documentation — a description of the structure, extension points, and examples of running tests for a new developer joining the project.
  • Testing regulations — when checks are run, which branches are protected from merging without green tests, how a failure is reported, and who is responsible for triage.
  • CI/CD instructions — step-by-step pipeline setup: installing dependencies, running suites, publishing the coverage report, and blocking the build on regression.

Each artifact is aligned with your infrastructure and verified in a real run — not a template from the documentation, but a working configuration for your module. Describe the task — we'll come back with an estimate and a handover plan.

Case: reducing regressions in the exchange module

What it was like before implementation

The module for exchanging data with an external system lived without automated tests: every change in field mapping or in the queue logic was checked manually — export a test dataset, run the exchange, compare balances and statuses.

This took about two hours per release, and regressions still slipped through: a mismatch in field types or an unhandled response from the external system surfaced already in production, and the investigation took up to a day.

What we did

We built a bootstrap for tests: we spun up the 1C-Bitrix core in an isolated environment, replaced the connection to the external system with a stub containing recorded responses, and captured the state of the test database before each run.

Then we covered three critical layers — parsing the incoming package, field mapping, and exchange error handling. The tests ran locally and in the build pipeline: a failure at any step blocked the merge.

Metrics before and after

Metric Before After
Release verification time ~2 hours manually 6-8 minutes automatically
Regressions in production per quarter 5-7 0-1
Exchange incident investigation up to a day minutes from the test log
Share of covered exchange scenarios about 15% 80%+

We also separately addressed rollback: if the external system changes the response format, the test fails before release, not after — the team sees exactly which scenario broke and fixes it in a targeted way.

Implementation practice has shown: the main value is not in the tests themselves, but in the fact that the exchange contract is fixed and has ceased to be a verbal agreement.

How is module testing different from site testing?

Site testing and module testing solve different problems, although both rely on PHPUnit. When the site as a whole is checked, the run boots the core, connects the installed modules, pulls the template and settings of a specific installation.

This scenario is closer to acceptance testing: it catches regressions at the junctions between components, but depends on the state of the database, cache, and access permissions — and fails for reasons unrelated to the code.

A module is tested in isolation. We initialize only its classes and the dependencies it declares explicitly, and replace calls to the core, information blocks, and external services with stubs.

Test isolation means that each suite runs in a clean environment and does not inherit data from the previous one: a test does not "leak" into the neighboring one and does not depend on the order of execution.

The key difference is in the dependency graph. For a site it is almost always implicit: a module works as long as the installation has the necessary neighbors and the connections are configured.

For a module, dependencies are described by contracts — what the module expects as input and what it guarantees as output. This makes it possible to verify the logic without booting the entire site and to find errors where they originated.

The speed of the run differs by an order of magnitude. A full suite for the site takes minutes, because each test boots the environment; module tests on stubs fit into seconds and can easily run on every commit.

That is why in CI it makes sense to separate the contours: fast module checks as a mandatory gate, heavy integration ones — on a schedule or before a release.

Isolation also gives predictability of failures: a failed module test almost always points to a specific method, not to "the site broke." That is precisely why module testing is laid down as the foundation of the pyramid, while site testing is left for end-to-end scenarios.

Technical doubts and limitations

Testing 1C-Bitrix modules runs into three real constraints: the kernel version, the state of the code, and the environment. Below is how we work around them in practice, without illusions about an "ideal" project.

The module was written for an old kernel version — will the tests even run?

Yes, if the bootstrap doesn't pull the entire kernel, but loads only the necessary modules via Loader::includeModule. We isolate the module under test from the rest of the distribution: we bring up a minimal set of dependencies and fix contracts through mocks.

On older branches, part of the API is marked as deprecated — the tests record the current behavior as a reference so that refactoring doesn't break what already works in production.

We have legacy code with no separation into layers. What can realistically be covered by tests?

We cover not everything, but the critical paths: calculations, access rights, order processing, integrations with 1C. Legacy code is often tied to global variables and direct database queries — we first wrap such places in a thin layer, then test them. This provides coverage of the key logic without rewriting the module entirely.

Will the tests run against the real database or a copy?

On a separate test database with fixtures that are rolled back after each run. This way the tests don't depend on production data and don't corrupt it. For integrations with 1C, we bring up stubs — we check the exchange logic, not the availability of the external service.

Can tests be run automatically on every commit?

Yes, once the bootstrap is stable. We set up a run in CI: building the environment, test database migrations, running the suite, reporting on failed tests. If a test is flaky, it doesn't go into the mandatory suite until the cause is eliminated.

What about tests that touch the Bitrix kernel directly?

We don't test the kernel — that's the vendor's domain. Our task is to verify that our module interacts with it correctly: event calls, working with the ORM, permissions.

Such checks go through the kernel's public interfaces, not through its internals, otherwise the tests will break when the platform is updated.

These limitations don't cancel testing — they define its boundaries. We honestly state what will be covered and what will remain for manual verification, and we record this in the plan.

Let's discuss your task and come back with a plan

To come back with an estimate and a plan rather than generalities, we need to understand the boundaries of the task: which modules we're covering, what's already in the repository, and what stage the project is at.

Then we'll align expectations on scope — from bootstrap and the first tests to a full pipeline running on every change.

What's worth preparing for the conversation:

  • A list of modules and components you want to cover with tests, marked by priority.
  • A link to the repository or an archive — we'll look at the structure, current dependencies, and how the build is set up.
  • A description of the environment: where the project runs, how it's deployed locally, whether there's a test environment.
  • Examples of "pain points": what breaks most often during updates, where manual testing consumes the most time.
  • Access requirements and infrastructure constraints — your own servers or cloud, whether there are isolated environments.
  • Expectations on timelines and on how the result will fit into your release process.

After the review, we'll come back with a project estimate: the scope of work, stages, checkpoints, and options — for example, starting with critical modules and expanding coverage, or laying down a full pipeline right away.

The next step is a short call based on the prepared data, after which we lock in the plan and start. Describe your task — we'll discuss the details and propose a solution tailored to your project.