Skip to content

<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>

Home | Changelog

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:coverage

Test Suites

The test configuration is defined in phpunit.xml.dist with four suites:

SuiteDirectoryBase ClassPurpose
Unittests/Unit/Advisable\Test\TestCasePure unit tests for domain services, REST resources, and PSR-compliant code. No CI bootstrap.
Integrationtests/Integration/Cache/, tests/Integration/CiBootstrapTest.phpAdvisable\Test\TestCase / Advisable\Test\CiTestCaseCache adapter tests and CI bootstrap smoke tests. No DB required.
Databasetests/Integration/Domains/, tests/Integration/Legacy/Advisable\Test\CiDatabaseTestCaseRepository and service tests against a real database. Tagged #[Group('database')].
Legacytests/Legacy/Advisable\Test\CiTestCaseLegacy 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 in phpunit.xml.dist). Sets up ENVIRONMENT, path constants (FCPATH, BASEPATH, APPPATH, VIEWPATH), Composer autoload, dotenv, the env() helper, timezone, and application constants. Used by all suites.
  • tests/bootstrap_ci.php -- CI-core bootstrap for Integration and Legacy suites. Loads CI's Common.php, charset/mbstring setup, CI core singletons ($GLOBALS), base controller, MX/Base.php (HMVC), and defines get_instance(). Lazily loaded by CiTestCase::setUpBeforeClass().

Base Classes

TestCase (tests/TestCase.php)

Base class for unit tests. Provides:

  • getFixture($name) -- loads fixture data from tests/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->db points at the test database via application/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:fast excludes them
  • Run via composer test:database (or vendor/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-reset

Environment variables (set in phpunit.xml.dist):

VariableDefaultDescription
TEST_DB_HOSTNAME127.0.0.1Test database host
TEST_DB_PORT3307Test database port
TEST_DB_DATABASEadveshop4_testTest database name
TEST_DB_USERNAMErootDatabase 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 tests

Database Integration Test Coverage (2883 tests)

Complete coverage across all domain entities:

DomainEntitiesTests
Cms13 (Page, Blog/*, Cookie, ContactEmail, Builder, Document, Subcontent, Video, Offer, OfferCategory)402
Product37 (Product, Category, Badge, Vendor, Line, Tag/, Attribute/, Bundle/, Variation/, ProductList/, Related/, and more)1003
Order9 (Order, OrderTag, Note, GiftCardOrder, Store, Vat, OrderBasket, DhlVoucher, SmartPoint)235
Marketplace9 (Iris, Jcc, Public, Shopflix, Skroutz orders + line items)189
Customer6 (Customer, Campaign, MessageHistory, Review, SmsMarketing, Tag)139
Promotion8 (Coupon + children, Gift + children)186
Transporter8 (Transporter + 7 children)181
Currency, Seo, Ai, Plus, Cart, Country, Slider, Event, Map18 entities548

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 ClassModuleWhat It Tests
AdvVatForOrderTesteshopDeliver/charge area types, receipt/invoice VAT computation
AdvVatValidateTesteshopGreek AFM checksum algorithm (mod 11 mod 10)
AdvPaymentsRegistryTesteshopType conversion (text/number/checkbox/multiselect), validation, roundtrip
AdvGiftsModelTesteshopProduct/combination/cart rule validators
AdvCouponsModelTestcouponsDiscount calculation by vendor/product/category/tag/cart with price ranges
AdvPriceTrackingGraphsTesteshopTimeline analysis, successive reductions, min/max prices
AdvJccTesteshopChecksum validation, parameter string generation, status calculation
AdvKlarnaPaymentsTesteshopPayload validation
AuthHelperTestauthallowRole, 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 calls get_instance() (e.g., BaseWriteRepository, BaseRepository). Do not try to stub get_instance() via eval() or globals — when the full unit suite runs, CiTestCase may have already loaded bootstrap_ci.php which defines a real get_instance() returning a CI controller with ->db = null. This causes TypeError on 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.dist is 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.xml is gitignored and can be created locally for developer-specific overrides — most commonly to point TEST_DB_PORT at the actual local MariaDB port (e.g. 3307). Copy phpunit.xml.dist to phpunit.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.