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
- Audit module code for testability, refactor dependency injection points.
- Set up PHPUnit with bootstrap for Bitrix environment.
- Write tests for business logic: calculations, state machines, parsers.
- Set up coverage report via Xdebug.
- Integrate test execution into CI (GitHub Actions / GitLab CI).
- 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.
Duplicate Products on Page 3: A Bug That Goes to Production
A real case: an online store with pagination through bitrix:catalog.section duplicates products on page three after every second visit. Cache clearing helps for a day, then the duplicates return. Root cause: a custom sort handler collides with PAGEN_1, and under a specific filter combination CIBlockElement::GetList returns identical IDs. Code review missed it; only testing caught it. We build QA for 1C-Bitrix projects that catches such bugs before they hit production: manual functional, automated E2E, load, and acceptance testing. With over a decade of Bitrix experience, we have a library of typical pitfalls and test scenarios that prevent these issues from the start.
How Does a Standard Bitrix Project Break Without Dedicated Testing?
1C-Bitrix is not a landing page. Behind the frontend lie dozens of modules, external integrations, and non‑obvious dependencies. A discount change in sale.discount breaks a promo code in sale.basket.discount — the discount module is one of the most fragile in the platform. Interchange with 1C via catalog.import.1c or REST fails when property mapping is off, resulting in products without price or stock. Core updates — bitrix:main updated, a custom component uses the deprecated CModule::IncludeModule. Without regression testing, every deployment is Russian roulette. Cross‑browser: sale.order.ajax renders differently in Safari and Chrome; the “Place Order” button can move off‑screen on an iPhone. These are not edge cases — they are daily realities for Bitrix teams.
What Does Functional Testing Cover?
We check every business scenario — not just “works or doesn’t work”, but all boundary cases.
Catalog (catalog.section, catalog.element)
- Smart filter
catalog.smart.filter: all property combinations, reset, result counting. Filters by SKUs break most often.
- Sorting + pagination — the duplicate bug described above.
- Comparison via
catalog.compare.list — add, remove, display differences.
- Quick view — modal window, cart from modal.
Cart and Order (sale.basket.basket, sale.order.ajax)
- Adding from catalog, product page, quick order.
- Discounts: by quantity, by amount, by coupon, cumulative. Discount intersection — at least eight test combinations.
- Delivery calculation: handlers
sale.delivery.services, cost, time, pickup points on map.
- Payment:
sale.paysystem — processing, handling declines, refunds.
- Order placement: email via
main.mail.event, CRM recording, transmission to 1C via sale.export.1c.
Personal Account (sale.personal.section)
- Registration, authorization, password recovery — including Cyrillic email edge cases.
- Order history, repeat order.
- Subscriptions, bonus program.
Forms and Search
-
form.result.new / iblock.element.add.form — submission, validation, file fields.
-
search.page — relevance, morphology, typo handling via search.title.
Why Is Regression Testing Critical for Bitrix?
After every deployment we verify that nothing previously working is broken.
-
Smoke tests — main page loads, catalog shows products, order completes. 5 minutes, run after every deploy. If smoke fails — roll back immediately.
-
Regression suite — 40–80 test cases covering main scenarios before every release.
-
Visual testing — screenshot comparison (Percy or Playwright). A button shifted 20px, font changed after update — test shows the diff.
-
Module checklists — structured lists for
sale, catalog, iblock, search. Each module has its own checklist.
What Happens During Load Testing?
The question is not “will the site handle it” but at how many concurrent users catalog.section starts returning 500 errors.
| Scenario |
Share |
Target Response |
What Breaks First |
| Main page |
20% |
< 1 sec |
Composite cache if not configured |
| Catalog with filters |
30% |
< 2 sec |
MySQL – heavy JOINs on b_iblock_element_property |
| Product page |
25% |
< 1.5 sec |
Queries for SKUs |
| Add to cart |
10% |
< 1 sec |
Table locks on b_sale_basket |
| Checkout |
5% |
< 3 sec |
Delivery handlers (external APIs) |
| Search |
10% |
< 2 sec |
b_search_content without indexes |
Tools:
-
k6 — JavaScript scripting.
- Apache JMeter — classic, for complex scenarios with cookie authorization.
- Yandex.Tank — real‑time visualization, integration with Overload.
Output: peak RPS, response times by percentiles p50/p95/p99, bottlenecks (CPU, RAM, MySQL slow queries on b_iblock_element, file cache). Recommendations: which index to add, which query to rewrite with D7 ORM, where to enable composite cache.
What Deliverables Do You Receive After Testing?
- Test plan with scope, priorities, and quality criteria.
- Test case suite — functional, regression, load.
- Defect report in a tracker (Jira/YouTrack) with severity classification.
- Auto tests (Playwright/Cypress) — basic smoke suite for CI/CD.
- Load testing protocol with graphs and recommendations.
- Acceptance certificate after UAT — confirming readiness for launch.
After delivery, we provide free consultation for a month — answering questions on test improvements and process adaptation. Contact us to receive a full package of documents and auto tests.
Cross‑Browser Testing
We test where buyers actually are. Statistics from your Metrica are more important than general market data.
Minimum set:
- Chrome (last 2 versions) — main traffic.
- Safari on iOS — critical for mobile checkout,
sale.order.ajax often behaves unpredictably.
- Yandex.Browser — significant share in Russia, Chromium‑based but with extension quirks.
- Samsung Internet — mobile Android, often forgotten.
Devices:
- Desktop: 1920×1080, 1366×768.
- iPhone: 375×812, 390×844 — checkout must be verified.
- Android: 360×800, 412×915.
Tools: BrowserStack for real devices, Playwright for automation on Chromium/Firefox/WebKit.
Automation
Playwright — primary choice for E2E on Bitrix:
- Cross‑browser: Chromium, Firefox, WebKit.
- Parallel execution, automatic waits.
- Works well with dynamic forms
sale.order.ajax.
- Supports mobile viewports and geolocation.
Cypress:
- Runs in browser — more stable for SPA‑like interfaces.
- Excellent visual runner for debugging.
- Limitation: only Chromium‑based browsers.
PHPUnit for custom code:
- Unit tests for custom Bitrix components and modules.
- Tests business logic without frontend dependency.
- Integration with CI/CD — GitLab CI, GitHub Actions.
UAT – Acceptance Testing
Final check with the client on a staging environment with real data:
- Jointly compile a list of critical scenarios — 15–20 key customer paths, not 200 test cases.
- Staging with a copy of the production database (anonymized personal data).
- Quick bug tracking — Jira/YouTrack, prioritization by severity.
- Acceptance protocol — document with results, signatures, and launch readiness.
Order UAT support and we guarantee a release without surprises.
QA Process – Integrated, Not Tacked On
-
Requirements analysis — QA participates in task discussions, catches ambiguities. “Does the discount apply to the product or the order?” — such a question upfront saves two days of debugging.
-
Test cases before development — scenarios ready before the first line of code.
-
Code review — checks for typical Bitrix mistakes: uncleared component cache, direct SQL queries instead of ORM, missing
$USER‑>IsAuthorized() check.
-
Functional → regression → deploy.
-
Post‑release monitoring — errors in
bitrix/error.log, metrics in Metrica, alerts for 500 errors.
We have been working with Bitrix for over 10 years and have tested more than 300 projects of various scales — from small online stores to corporate portals with 1C and Bitrix24 integration.
Timelines
| Task |
Duration |
| Test plan |
2–3 days |
| Functional testing (medium store) |
3–5 days |
| Basic E2E auto test suite (Playwright) |
2–3 weeks |
| Load testing + report |
1–2 weeks |
| Cross‑browser testing |
2–3 days |
| UAT support |
3–5 days |
| QA process from scratch |
3–4 weeks |
Testing cost is calculated individually for your project. Get a free consultation — we will assess the scope within one business day and provide a preliminary estimate and test plan. Contact us to discuss your project and schedule a call.