Test Framework
The test framework lives in not_for_release/testFramework. It is present in the Git repository and is not included in release zip downloads.
Feature tests are destructive. They rebuild and reseed test databases, so do not point them at a real store database.
Current test stack
- PHPUnit 11 is used for all suites.
phpunit.xmldefines three suites:Unit,FeatureStore, andFeatureAdmin.- Composer installs only development and test dependencies. Zen Cart production bootstrap still uses its own runtime autoloading.
- Feature tests run in-process through helpers under
not_for_release/testFramework/Support/InProcess/.
Install dependencies
From the Zen Cart project root:
composer install
That installs PHPUnit and the test-only dependencies declared in composer.json.
Composer commands
The supported Composer scripts are:
composer tests-unitcomposer tests-featurecomposer tests-feature-storecomposer tests-feature-admincomposer tests-feature-parallelcomposer tests-feature-store-parallelcomposer tests-feature-admin-parallelcomposer tests-feature-admin-plugin-filesystemcomposer tests-plugincomposer tests-cicomposer tests-ci-localcomposer tests-db-prepare-workerscomposer tests-report-feature-groupscomposer tests-report-feature-groups-strictcomposer tests-runtime-describe
Most runner scripts accept normal PHPUnit arguments after --.
The Composer commands are convenience aliases for shell scripts in not_for_release/testFramework/. You can also run those scripts directly.
composer tests-ci runs the top-level CI-style flow as-is, using the currently resolved environment and test profile.
composer tests-ci-local runs the same flow but applies local worker-database defaults when they are not already set, currently:
ZC_TEST_DB_BASE_NAME=dbZC_TEST_DB_WORKERS=2ZC_TEST_DB_INCLUDE_BASE=0
composer tests-db-prepare-workers is lower-level. It prepares or previews the worker databases expected by the parallel feature runners, but it does not run the test suites itself.
Examples of direct script usage:
bash not_for_release/testFramework/run-tests-ci.sh
bash not_for_release/testFramework/run-store-feature-tests-ci.sh --filter SearchInProcessTest
bash not_for_release/testFramework/prepare-worker-databases.sh --dry-run
bash not_for_release/testFramework/report-feature-test-groups.sh --summary-only
Using Composer is usually the simpler choice:
- shorter commands
- script names are centralized in
composer.json - easier for contributors who expect the documented
composer tests-*entrypoints
Running the scripts directly is useful when you need more control:
- clearer visibility into which underlying runner is being executed
- easier to call a specific shell script while debugging
- convenient in CI or ad-hoc shell automation where you want to bypass the Composer alias layer
- avoids Composer script timeout behavior in environments where Composer is configured with a process time limit
The tradeoff is mostly ergonomics. Composer is easier to remember and document, while direct script execution is more explicit and sometimes easier to debug.
Examples:
composer tests-unit -- --filter RuntimeConfigTest
composer tests-feature-store -- --filter SearchInProcessTest
composer tests-feature-admin -- --dry-run --filter AdminEndpointsTest
composer tests-plugin -- --plugin gdpr-dsar --suite FeatureAdmin
composer tests-db-prepare-workers -- --dry-run
Unit tests
Run unit tests with:
composer tests-unit
Unit tests live under not_for_release/testFramework/Unit/ and normally extend:
Tests\Support\zcUnitTestCase
zcUnitTestCase initializes the unit-test bootstrap for you in setUp().
Feature tests
Feature tests exercise the application through Zen Cart’s bootstrap, request handling, database setup, and HTML responses.
Current feature suites live in:
not_for_release/testFramework/FeatureStorenot_for_release/testFramework/FeatureAdmin
Run them with:
composer tests-feature
composer tests-feature-store
composer tests-feature-admin
Useful behaviors of the current runners:
composer tests-feature-storeprints the resolved runtime, validates feature grouping, prepares worker databases, then runs storefront parallel tests.composer tests-feature-admindoes the same for admin tests, then also runs adminplugin-filesystembuckets.composer tests-featureruns the aggregate storefront/admin parallel flow plus plugin-filesystem buckets.--dry-runshows what would run without mutating the databases.
Base classes
For new feature tests, use these base classes:
Tests\Support\zcInProcessFeatureTestCaseStoreTests\Support\zcInProcessFeatureTestCaseAdmin
The compatibility aliases below still exist, but the in-process classes are the current implementation:
Tests\Support\zcFeatureTestCaseStoreTests\Support\zcFeatureTestCaseAdminTests\Support\zcFeatureTestCase
The storefront base class provides helpers like get(), post(), getMainPage(), visitLogin(), visitCart(), and redirect/cookie handling.
The admin base class provides helpers like getAdmin(), postAdmin(), visitAdminHome(), visitAdminCommand(), submitAdminLogin(), and submitAdminForm().
Feature test grouping
The current shell runners use PHPUnit groups to decide what can run in parallel.
- Tag parallel-safe feature tests with
parallel-candidate. - Tag filesystem-mutating plugin tests with
plugin-filesystem. - Use
serialtogether withplugin-filesystemwhen the test must not run concurrently.
Example with PHPUnit 11 attributes:
#[\PHPUnit\Framework\Attributes\Group('parallel-candidate')]
final class SearchInProcessTest extends \Tests\Support\zcInProcessFeatureTestCaseStore
{
}
#[\PHPUnit\Framework\Attributes\Group('serial')]
#[\PHPUnit\Framework\Attributes\Group('plugin-filesystem')]
final class BasicPluginInstallTest extends \Tests\Support\zcInProcessFeatureTestCaseAdmin
{
}
Use:
composer tests-report-feature-groups
composer tests-report-feature-groups-strict
to inspect or enforce grouping coverage.
Understanding the group report
composer tests-report-feature-groups prints both explicit grouping tags and a few heuristic shared-state signals.
Common report sections:
Tagged serial: feature tests explicitly markedserial.Tagged plugin-filesystem: feature tests explicitly markedplugin-filesystem.Tagged parallel-candidate: feature tests explicitly markedparallel-candidate.Untagged files: feature tests with none of the expected explicit grouping tags.Plugin-local feature test files: feature tests discovered underzc_plugins/*/*/tests/FeatureStoreorzc_plugins/*/*/tests/FeatureAdmin.Suite breakdown: per-suite totals for store and admin tests, including how many are serial, parallel-candidate, or untagged.Invalid explicit group combinations: files with conflicting or incomplete explicit tags, such asserialtogether withparallel-candidate, orplugin-filesystemwithoutserial.
The report also includes heuristic sections. These are grep-style indicators, not perfect proofs:
Heuristic direct DB writers: files containing direct calls such asTestDb::insert(),TestDb::update(), orTestDb::truncate().Heuristic custom seeder users: files callingrunCustomSeeder().Heuristic filesystem writers: files that appear to mutate plugin or filesystem state, such as plugin installation/removal helpers or direct file writes liketouch(),file_put_contents(), orunlink().
These heuristic buckets are there to highlight tests that may need serial treatment or closer review. They can miss some shared-state behavior and they can also over-report harmless matches.
composer tests-report-feature-groups-strict is the stricter form used by the runners. It fails when feature test files are untagged or when explicit grouping combinations are invalid.
Configuration and environment
The current framework still uses test configure profiles named by user or runtime, such as <user>.store.configure.php, <user>.admin.configure.php, ddev.store.configure.php, or runner.store.configure.php.
Instead, runner settings are resolved from:
- environment variables already exported in your shell or CI job
- the active Zen Cart test profile, resolved from the test configure files when DB settings are not overridden
Environment values exported in the shell or CI job override the database defaults derived from the active test profile.
Useful variables:
ZC_TEST_DB_HOSTZC_TEST_DB_PORTZC_TEST_DB_USERZC_TEST_DB_PASSWORDZC_TEST_DB_BASE_NAMEZC_TEST_DB_WORKERSZC_TEST_DB_INCLUDE_BASEZC_TEST_USE_MAILSERVERZC_TEST_MAILSERVER_HOSTZC_TEST_MAILSERVER_PORTZC_TEST_MAILSERVER_USERZC_TEST_MAILSERVER_PASSWORDZC_PARALLEL_PROCESSESZC_TEST_PROGRESS_INTERVAL
To inspect the effective runtime configuration:
composer tests-runtime-describe
That command reports the resolved config profile, config files, worker token, database name, log directory, artifact directories, and plugin directory.
Test configure files
Feature bootstrap still loads dedicated test configure files through not_for_release/testFramework/Support/application_testing.php.
The resolver looks in not_for_release/testFramework/Support/configs/ for the active test config profile and loads:
runner.main.configure.phprunner.store.configure.phprunner.admin.configure.php
Those files provide the runtime-specific configure.php values used during feature-test bootstrap.
Databases, logs, and artifacts
Prepare worker databases explicitly with:
composer tests-db-prepare-workers
The MySQL user used for feature-test database preparation should normally have permission to create and drop databases, because the worker-database setup may issue DROP DATABASE and CREATE DATABASE statements.
If your test user does not have those privileges, pre-create the worker databases with a privileged MySQL user before running the feature suite.
Preview the database plan without changing MySQL:
composer tests-db-prepare-workers -- --dry-run
Current feature runners also call worker-database preparation automatically before they execute tests.
Logs and artifacts are written under not_for_release/testFramework/logs/. The runtime helper also reports the resolved artifact directories for storefront and admin tests.
Plugin-local tests
Plugins can keep tests inside their own versioned directory:
zc_plugins/<PluginName>/<version>/tests
Supported suite layout:
tests/Unittests/FeatureStoretests/FeatureAdmin
Recommended plugin-local base classes:
Tests\Support\zcUnitTestCaseTests\Support\zcInProcessFeatureTestCaseStoreTests\Support\zcInProcessFeatureTestCaseAdmin
Run all plugin-local tests:
composer tests-plugin
Run a single plugin or suite:
composer tests-plugin -- --plugin gdpr-dsar
composer tests-plugin -- --plugin gdpr-dsar --suite FeatureStore
composer tests-plugin -- --plugin gdpr-dsar --suite FeatureAdmin
composer tests-plugin -- --plugin gdpr-dsar --suite Unit
Run only plugin-local filesystem-mutation tests:
composer tests-plugin -- --plugin gdpr-dsar --require-group plugin-filesystem --group plugin-filesystem
Seeders and database customization
The framework bootstraps feature databases from the install SQL and test support seeders. Custom seeders live in:
not_for_release/testFramework/Support/database/Seeders/
Seeder classes are autoloaded with the Seeders\ namespace and must implement Tests\Services\Contracts\TestSeederInterface.
Use a custom seeder when a test needs database state beyond the standard bootstrap, for example:
- a specific configuration value
- extra products, coupons, or tax data
- a setup sequence that would be noisy or repetitive to build inline in every test
The seeder interface is simple:
namespace Tests\Services\Contracts;
interface TestSeederInterface
{
public function run(array $parameters = []): void;
}
A typical seeder updates or inserts rows using Tests\Support\Database\TestDb.
Example:
<?php
namespace Seeders;
use Tests\Services\Contracts\TestSeederInterface;
use Tests\Support\Database\TestDb;
class StoreWizardSeeder implements TestSeederInterface
{
public function run(array $parameters = []): void
{
TestDb::update(
'configuration',
['configuration_value' => 'Zencart Store Name'],
'configuration_key = :config_key',
[':config_key' => 'STORE_NAME']
);
TestDb::update(
'configuration',
['configuration_value' => 'Zencart Store Owner'],
'configuration_key = :config_key',
[':config_key' => 'STORE_OWNER']
);
}
}
From a test case that uses the database concerns helpers, run a custom seeder with:
self::runCustomSeeder('StoreWizardSeeder');
That call resolves the class as Seeders\StoreWizardSeeder and executes its run() method.
A typical usage pattern in a feature test looks like this:
final class ExampleStoreTest extends \Tests\Support\zcInProcessFeatureTestCaseStore
{
public function test_store_uses_seeded_configuration(): void
{
self::runCustomSeeder('StoreWizardSeeder');
$response = $this->get('/');
$response->assertOk();
$response->assertSee('Zencart Store Name');
}
}
Keep seeders focused and test-specific. If a seeder becomes broadly useful across many tests, give it a descriptive name and keep the setup logic reusable rather than embedding one-off assertions or test flow inside the seeder itself.
Container-based runs
The preferred repeatable CI runtime is the published test-runner container:
ghcr.io/zencart/zencart-test-runner
Example:
docker run --rm \
-v "$PWD:/var/www/html" \
-w /var/www/html \
ghcr.io/zencart/zencart-test-runner:php-8.4 \
composer tests-unit
The container supplies PHP and tooling. Zen Cart source code is still mounted from your checkout.
Notes
- Unit tests do not require the feature-test database setup.
- Feature tests do not run browser JavaScript.
- The
zc_installdirectory must be present for database bootstrap and demo-data loading.