Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/Testing.md for this page in Markdown format</div>
Testing Guide
AdvEshop v4 uses PHPUnit 10.5 for automated testing. A single phpunit.xml.dist defines four named testsuites: Unit, Integration, Database, and Legacy. The full run executes ~5,400 tests; database tests need a populated test DB.
Quick Start
bash
# Run everything (requires a populated test DB)
composer test
# Fast inner loop — skip the database group, no DB required
composer test:fast
# Run a single suite
composer test:unit # tests/Unit
composer test:integration # tests/Integration/Cache + CiBootstrapTest
composer test:database # tests/Integration/Domains + tests/Integration/Legacy
composer test:legacy # tests/Legacy
# Run a specific test class
vendor/bin/phpunit --filter=CacheItemTest
# Coverage (Unit suite)
composer test:coverageTest Suites
The test configuration is defined in phpunit.xml.dist with four suites:
| Suite | Directory | Base Class | Purpose |
|---|---|---|---|
| Unit | tests/Unit/ | Advisable\Test\TestCase | Pure unit tests for domain services, REST resources, and PSR-compliant code. No CI bootstrap. |
| Integration | tests/Integration/Cache/, tests/Integration/CiBootstrapTest.php | Advisable\Test\TestCase / Advisable\Test\CiTestCase | Cache adapter tests and CI bootstrap smoke tests. No DB required. |
| Database | tests/Integration/Domains/, tests/Integration/Legacy/ | Advisable\Test\CiDatabaseTestCase | Repository and service tests against a real database. Tagged #[Group('database')]. |
| Legacy | tests/Legacy/ | Advisable\Test\CiTestCase | Legacy HMVC module / library tests via reflection. CI bootstrap, no DB. |
Suite placement rule of thumb: if your test extends CiDatabaseTestCase, place it under tests/Integration/Domains/{Context}/ (or tests/Integration/Legacy/{Module}/ for HMVC code). Pure PHP tests go in tests/Unit/. Reflection-based legacy tests go in tests/Legacy/.
The Database suite is opt-out by group: composer test:fast runs phpunit --exclude-group=database, which skips every test extending CiDatabaseTestCase regardless of which directory it lives in.
Bootstrap Files
tests/bootstrap.php-- PHPUnit bootstrap (configured inphpunit.xml.dist). Sets upENVIRONMENT, path constants (FCPATH,BASEPATH,APPPATH,VIEWPATH), Composer autoload, dotenv, theenv()helper, timezone, and application constants. Used by all suites.tests/bootstrap_ci.php-- CI-core bootstrap for Integration and Legacy suites. Loads CI'sCommon.php, charset/mbstring setup, CI core singletons ($GLOBALS), base controller, MX/Base.php (HMVC), and definesget_instance(). Lazily loaded byCiTestCase::setUpBeforeClass().
Base Classes
TestCase (tests/TestCase.php)
Base class for unit tests. Provides:
getFixture($name)-- loads fixture data fromtests/data/fixtures/
CiTestCase (tests/CiTestCase.php)
Base class for integration and legacy tests. Extends TestCase and adds:
- Lazy CI boot via
setUpBeforeClass()-- CodeIgniter is only initialized once per test class - Output capture helpers
- Request simulation helpers
- Automatic tearDown resets to prevent state leakage between tests
CiDatabaseTestCase (tests/CiDatabaseTestCase.php)
Base class for CI3-aware database integration tests. Extends CiTestCase and adds:
- CI3 database connection --
$CI->dbpoints at the test database viaapplication/config/testing/database.php - Transaction-per-test isolation -- using CI3's
trans_begin()/trans_rollback() db()helper -- convenient access to CI3's query builder- Graceful skip if test database is not configured
- Tests inherit
#[Group('database')]from the base class —composer test:fastexcludes them - Run via
composer test:database(orvendor/bin/phpunit --testsuite=Database)
php
<?php
namespace Advisable\Test\Integration\Domains\Product;
use Advisable\Test\CiDatabaseTestCase;
use PHPUnit\Framework\Attributes\Test;
class ProductRepositoryTest extends CiDatabaseTestCase
{
#[Test]
public function testFindsProductById(): void
{
$this->db()->insert('shop_product', ['id' => 99999, 'vat_id' => 1]);
$repo = di()->get(Repository::class);
$product = $repo->get(99999);
$this->assertNotNull($product);
}
}Test Database Setup
Database integration tests require a test database. The Docker Compose file provides a MariaDB instance on a RAM disk:
bash
# Start test database
.docker/integration/compose.sh up -d
# Run migrations and seed
composer test:db-migrate
composer test:db-seed
# Run database integration tests
composer test:database
# Full reset (rollback + migrate + seed)
composer test:db-resetEnvironment variables (set in phpunit.xml.dist):
| Variable | Default | Description |
|---|---|---|
TEST_DB_HOSTNAME | 127.0.0.1 | Test database host |
TEST_DB_PORT | 3307 | Test database port |
TEST_DB_DATABASE | adveshop4_test | Test database name |
TEST_DB_USERNAME | root | Database user |
TEST_DB_PASSWORD | (empty) | Database password |
Test Hierarchy
TestCase (tests/TestCase.php)
├── DatabaseTestCase (tests/DatabaseTestCase.php) ← PDO test DB, no CI3 (raw SQL, schema)
├── CiTestCase (tests/CiTestCase.php) ← CI3 framework, no DB (legacy mocks)
│ └── CiDatabaseTestCase (tests/CiDatabaseTestCase.php) ← CI3 + test DB (repos, models, services)
└── (direct extension) ← Pure unit testsDatabase Integration Test Coverage (2883 tests)
Complete coverage across all domain entities:
| Domain | Entities | Tests |
|---|---|---|
| Cms | 13 (Page, Blog/*, Cookie, ContactEmail, Builder, Document, Subcontent, Video, Offer, OfferCategory) | 402 |
| Product | 37 (Product, Category, Badge, Vendor, Line, Tag/, Attribute/, Bundle/, Variation/, ProductList/, Related/, and more) | 1003 |
| Order | 9 (Order, OrderTag, Note, GiftCardOrder, Store, Vat, OrderBasket, DhlVoucher, SmartPoint) | 235 |
| Marketplace | 9 (Iris, Jcc, Public, Shopflix, Skroutz orders + line items) | 189 |
| Customer | 6 (Customer, Campaign, MessageHistory, Review, SmsMarketing, Tag) | 139 |
| Promotion | 8 (Coupon + children, Gift + children) | 186 |
| Transporter | 8 (Transporter + 7 children) | 181 |
| Currency, Seo, Ai, Plus, Cart, Country, Slider, Event, Map | 18 entities | 548 |
Each test file covers:
- RepositoryTest:
get(),match(),matchOne(),count(), filters, sorts, pagination, relation loading (translations, BT, O2M, M2M) - ServiceTest:
all(),item(),get(),WriteService::create(),update(),delete(), round-trip lifecycle
Test Coverage by Area
Unit Tests (~1936 tests)
Domain Service Tests (~94 tests): Cover all 15 domain contexts -- Product, Order, Customer, Promotion, Transporter, Cms, Marketplace, and more. These test service-level business logic in isolation.
Repository Relation Loading Tests (~36 tests): Characterization tests (tests/Unit/Domains/Support/Repository/RelationLoading/) pin the behavior of OneToMany, BelongsTo, HasOne, ManyToMany, recursive, nested, and sort-aware relation loading. Unit tests (tests/Unit/Domains/Support/Repository/RelationLoader/) verify the extracted strategy classes (OneToManyLoader, BelongsToLoader, HasOneLoader, ManyToManyLoader, RelationLoaderRegistry) in isolation.
REST Resource/Collection Tests (~274 tests): Verify that entities are correctly transformed into API response format by Resource and Collection classes.
HandlesRestfulActions Tests: Integration-level tests for the standard REST controller actions (index, show, item) with mocked Service dependencies.
CI Bootstrap Smoke Tests: Verify that constants, core functions, configuration loading, and CI singletons initialize correctly.
Legacy Integration Tests (197 tests)
| Test Class | Module | What It Tests |
|---|---|---|
AdvVatForOrderTest | eshop | Deliver/charge area types, receipt/invoice VAT computation |
AdvVatValidateTest | eshop | Greek AFM checksum algorithm (mod 11 mod 10) |
AdvPaymentsRegistryTest | eshop | Type conversion (text/number/checkbox/multiselect), validation, roundtrip |
AdvGiftsModelTest | eshop | Product/combination/cart rule validators |
AdvCouponsModelTest | coupons | Discount calculation by vendor/product/category/tag/cart with price ranges |
AdvPriceTrackingGraphsTest | eshop | Timeline analysis, successive reductions, min/max prices |
AdvJccTest | eshop | Checksum validation, parameter string generation, status calculation |
AdvKlarnaPaymentsTest | eshop | Payload validation |
AuthHelperTest | auth | allowRole, mergeUsersRoles, allowedUsers |
Writing New Tests
Unit Tests
Place unit tests in tests/Unit/ and extend Advisable\Test\TestCase:
php
<?php
declare(strict_types=1);
namespace Advisable\Test\Unit\Domains\Product;
use Advisable\Test\TestCase;
class MyServiceTest extends TestCase
{
public function testSomething(): void
{
// Pure PHP test, no CI dependency
$this->assertTrue(true);
}
}Legacy/Integration Tests
Place tests in tests/Legacy/ or tests/Integration/ and extend Advisable\Test\CiTestCase:
php
<?php
declare(strict_types=1);
namespace Advisable\Test\Legacy;
use Advisable\Test\CiTestCase;
class MyLegacyTest extends CiTestCase
{
public function testLegacyLibrary(): void
{
// CI is available via get_instance()
$ci =& get_instance();
// ...
}
}Patterns for Testing Legacy Code
Legacy CodeIgniter classes often have constructors that depend on the CI super-object. Use these patterns to test them in isolation:
Bypass constructor with Reflection:
php
$ref = new \ReflectionClass(SomeLegacyClass::class);
$instance = $ref->newInstanceWithoutConstructor();Important: Always use
newInstanceWithoutConstructor()for classes whose constructor callsget_instance()(e.g.,BaseWriteRepository,BaseRepository). Do not try to stubget_instance()viaeval()or globals — when the full unit suite runs,CiTestCasemay have already loadedbootstrap_ci.phpwhich defines a realget_instance()returning a CI controller with->db = null. This causesTypeErroron typed properties. Bypassing the constructor and injecting dependencies via reflection avoids the issue entirely.
Access protected/private methods:
php
$method = new \ReflectionMethod(SomeLegacyClass::class, 'protectedMethod');
$method->setAccessible(true);
$result = $method->invoke($instance, $arg1, $arg2);Register Singleton test instances:
For classes using the Singleton pattern, register a test instance via reflection on Singleton::$instances.
Mock dependencies with anonymous classes:
php
$mockRegistry = new class {
public function value(string $key) {
return match ($key) {
'some_setting' => 'test_value',
default => null,
};
}
};Configuration
phpunit.xml.dist vs phpunit.xml
phpunit.xml.distis committed to the repository and contains shared defaults — four named testsuites, the env var defaults (DB host/port/name, deferred-task disable flags), and PHPUnit's strict flags (failOnRisky,failOnWarning,beStrictAboutOutputDuringTests). Do not modify this for local preferences.phpunit.xmlis gitignored and can be created locally for developer-specific overrides — most commonly to pointTEST_DB_PORTat the actual local MariaDB port (e.g. 3307). Copyphpunit.xml.disttophpunit.xml, edit the override, and PHPUnit will prefer the local file automatically.
Fixtures
Test fixtures are stored in tests/data/fixtures/ and loaded via $this->getFixture('fixture_name') from the base TestCase class.
CI/CD
Pipeline CI test steps are not yet integrated — bitbucket-pipelines.yml currently only builds Docker images. Tests should be run locally by developers and AI agents before committing. Wiring composer test into the pipeline (with a MariaDB service container for the Database suite) is tracked as a follow-up.