Writing Integration Tests for 1C-Bitrix
You deploy an update, and in an hour discover that 1C import stopped creating new products — method signature changed in catalog module, but integration code wasn't updated. Integration tests solve exactly this problem: they verify your code correctly interacts with Bitrix core, database and external systems. Not unit-tests in vacuum, but real scenarios with real core.
How Integration Tests Differ from Unit Tests in Bitrix Context
Unit-testing Bitrix project is useless without mocking half of core. Class calling CIBlockElement::GetList() depends on iblocks, DB, cache, access rights. Mocking all this — writing second Bitrix. Integration tests load real kernel, work with real (test) DB and check full cycle.
Typical scenarios:
- Creating order via
CSaleOrder::Add()/\Bitrix\Sale\Order::create()— verify all event handlers trigger, discounts apply, status correct - XML product import — verify field mapping, section creation, price update
- Generating 1C export — verify XML structure, data correctness
- Processing payment system webhook — verify order status change
Test Environment Setup
PHPUnit + Bitrix kernel. Bitrix doesn't ship with test infrastructure, setup manually.
File bootstrap.php for PHPUnit:
$_SERVER['DOCUMENT_ROOT'] = '/path/to/site';
$_SERVER['HTTP_HOST'] = 'test.local';
$_SERVER['SERVER_NAME'] = 'test.local';
$GLOBALS['DBType'] = 'mysql'; // or pgsql
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_NO_ACCELERATOR_RESET', true);
define('STOP_STATISTICS', true);
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
Constants NO_KEEP_STATISTIC and STOP_STATISTICS disable statistics logging, NOT_CHECK_PERMISSIONS — permission check (otherwise tests depend on current user).
Test DB. Two approaches:
- Separate DB — copy of production schema without data. Safe, but requires schema sync maintenance.
-
Transactions — each test wrapped in transaction, rolled back in
tearDown(). Fast, but doesn't work for tests using transactions themselves (nested transactions in MySQL behave unexpectedly).
Recommended approach for Bitrix — transactions with fallback to manual cleanup:
protected function setUp(): void
{
\Bitrix\Main\Application::getConnection()->startTransaction();
}
protected function tearDown(): void
{
\Bitrix\Main\Application::getConnection()->rollbackTransaction();
}
Test Structure
Place tests in /local/tests/ with mirrored structure:
/local/tests/
Integration/
Catalog/
ImportTest.php → product import tests
PriceCalculationTest.php
Sale/
OrderCreationTest.php
DiscountTest.php
Exchange/
OneCExportTest.php
bootstrap.php
phpunit.xml
phpunit.xml:
<phpunit bootstrap="bootstrap.php">
<testsuites>
<testsuite name="Integration">
<directory>Integration</directory>
</testsuite>
</testsuites>
</phpunit>
Writing Tests: Bitrix Patterns
Order creation test:
public function testOrderCreationWithDiscount(): void
{
// Arrange: create test product and discount
$productId = $this->createTestProduct('TEST-001', 1000);
$discountId = $this->createTestDiscount(10); // 10%
// Act: create order
$order = \Bitrix\Sale\Order::create('s1', 1);
$basket = \Bitrix\Sale\Basket::create('s1');
$item = $basket->createItem('catalog', $productId);
$item->setFields(['QUANTITY' => 1, 'CURRENCY' => 'RUB', 'PRODUCT_PROVIDER_CLASS' => '\CCatalogProductProvider']);
$order->setBasket($basket);
$order->doFinalAction(true);
$result = $order->save();
// Assert
$this->assertTrue($result->isSuccess());
$this->assertEquals(900, $order->getPrice()); // 1000 - 10%
}
XML import test:
Don't call CIBlockCMLImport directly — it's heavy and poorly controlled. Test your wrapper code calling Bitrix API:
public function testProductImportCreatesElement(): void
{
$importer = new \Project\Import\ProductImporter(CATALOG_IBLOCK_ID);
$result = $importer->import([
'XML_ID' => 'TEST-IMPORT-001',
'NAME' => 'Test product',
'PRICE' => 500,
]);
$this->assertTrue($result->isSuccess());
$element = \CIBlockElement::GetList(
[],
['IBLOCK_ID' => CATALOG_IBLOCK_ID, 'XML_ID' => 'TEST-IMPORT-001'],
false, false, ['ID', 'NAME']
)->Fetch();
$this->assertNotFalse($element);
$this->assertEquals('Test product', $element['NAME']);
}
Event Handler Handling in Tests
Bitrix event handlers (OnBeforeIBlockElementAdd, OnSaleOrderBefore, etc.) — source of unexpected behavior. Handler registered in init.php fires in test environment too.
Two approaches:
- Accept as given — test system completely, including handlers. More correct for integration testing.
-
Temporarily disable —
RemoveEventHandler()insetUp(), restore intearDown(). Use when handler calls external service (email sending, API request).
What NOT to Test Integrally
- Rendering and visual display — E2E-test job (Playwright, Selenium)
- Pure business logic without Bitrix dependencies — unit-test job
- Performance — integration tests slow by definition, use separate benchmark framework
Timeline for Test Writing
| Coverage | Volume | Timeline |
|---|---|---|
| Critical path (checkout, import) | 15-25 tests | 3-5 days |
| Core functionality | 50-80 tests | 1-2 weeks |
| Extended coverage (edge cases, errors) | 100+ tests | 3-4 weeks |
Start with tests for places that break most often. Usually this is 1C import/export and price calculation with discounts.







