Testing
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/. - Infection is available for mutation testing of selected classes against the unit tests. It is a developer aid and is not run in CI. See Mutation testing.
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-describecomposer infectioncomposer infection-fast
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().
Mutation testing
Line coverage tells you which code a test ran, not whether the test would notice if that code were wrong. Infection fills that gap. It makes small changes (“mutants”) to the production code, such as removing a concatenated path segment, flipping a comparison, or dropping an rtrim(), and re-runs the unit tests. A mutant that no test fails on has “escaped”. Each escaped mutant points to behavior that is not pinned down by a test.
Mutation testing is a developer aid for finding gaps while you write tests. It is not run in CI, and the score is not a gate for merging.
Requirements
Infection needs a code-coverage driver, either PCOV (preferred, much faster) or Xdebug in coverage mode. The infection/infection package is installed by composer install.
With DDEV, PCOV can be added to the web container with a small override file such as .ddev/config.pcov.yaml:
webimage_extra_packages: ["php${DDEV_PHP_VERSION}-pcov"]
then run ddev restart. Run Infection inside the container, for example ddev composer infection.
Infection also downloads the PHPUnit XML schema referenced in phpunit.xml from schema.phpunit.de, so a run fails if that site is unavailable.
Running it
Run it over the full configured scope:
composer infection
or only over the lines you have changed relative to master, which is much quicker and is usually what you want while working on a branch:
composer infection-fast
You can also limit a run to particular source files by passing them as arguments, and pass any other Infection option after --:
composer infection -- includes/classes/Category.php includes/classes/category_tree.php
The summary prints in the terminal. Detailed reports, including an HTML report that shows each escaped mutant as a diff, are written to tmp/infection/:
tmp/infection/infection.htmltmp/infection/infection.logtmp/infection/summary.log
A full run over the current scope takes roughly 10 to 15 minutes. Mutants are run in parallel, using four threads by default.
How it is set up
Configuration lives in infection.json5 in the project root.
- Scope. Infection only mutates an allow-list of files under
includes/classes. The allow-list is a single regular expression insource.excludes, because Infection accepts directories but not individual files as sources. Everything not named in the expression is excluded. - Tests. Infection runs the
Infectionsuite defined inphpunit.xml, not the wholeUnitsuite. The full unit suite cannot share one PHPUnit process (see the notes on global constants and function declarations above), so theInfectionsuite lists the test directories that cover the mutated classes and runs with--process-isolation. - Coverage. The Composer scripts pass
--only-covering-test-cases, so each mutant is tested only by the tests that cover it. - Bootstrap. Infection loads the classes it mutates in its own process, which does not run the PHPUnit bootstrap.
not_for_release/testFramework/Support/infection-bootstrap.phpdefines the constants some classes need in order to load, for exampleIS_ADMIN_FLAG. - Thresholds.
minMsiandminCoveredMsiare set slightly below the current score so that a regression is flagged. The MSI (Mutation Score Indicator) is the percentage of mutants that were detected.
Adding a class to the scope
- Make sure the class has unit tests that run reliably, and that they are in a test directory included in the
Infectionsuite inphpunit.xml. Add the directory if it is not. - Add the file or directory to the allow-list expression in
source.excludesininfection.json5. - Run Infection against just that file to get a baseline, then work through the escaped mutants.
Working through escaped mutants
Open tmp/infection/infection.html or the log and read the diff for each escaped mutant. Typical fixes:
- Assert the exact value, path, or SQL string a method builds, not just that it returns something.
- Test both sides of a condition, including boundary values and empty inputs.
- Check ordering and de-duplication of lists, and that array keys are re-indexed.
- Call protected methods through a test subclass or a bound closure, so that a mutant that narrows their visibility is detected.
Not every escaped mutant is a real gap. Some are equivalent, meaning the changed code behaves identically, for example an extra rtrim() whose effect is repeated by the next step, or an early return on an empty array. Review each one and move on when it cannot be observed. Do not add contrived tests just to raise the score.
Test helpers
Two helpers under not_for_release/testFramework/Support/ make it easier to unit test classes that use the global $db:
Tests\Support\ScriptedDatabaseis a trait that replaces$dbwith a mockedqueryFactory. Register responses withrespondTo($regex, $rows); the first matching pattern wins. Executed SQL is recorded so a test can assert on the exact statement and on the limit, cache flag, and cache lifetime passed toExecute().Tests\Support\InMemoryTemplateSelectDatabaseis a trait that models thetemplate_selecttable in memory for theTemplateSelecttests.
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.
This tab describes the test framework as it exists on the 2.2 branch (and 2.1, where noted). The framework lives in not_for_release/testFramework and is only present if you clone the Zen Cart repository with git. It is not included in release zip downloads.
Warning: Feature tests rebuild the database they are pointed at, on every test class. Never point them at a database that holds real store data. Use a dedicated test database.
How it works
- PHPUnit 9 runs three suites, defined in
phpunit.xml:Unit,FeatureStoreandFeatureAdmin. - Unit tests run in the PHPUnit process with
--process-isolation. They need no database and no web server. - Feature tests drive a real, running copy of Zen Cart over HTTP using Symfony’s
HttpBrowser. The test code fetches pages and submits forms fromHTTP_SERVER, and checks the HTML that comes back. They do not run JavaScript. - Before each feature test class, the framework drops and rebuilds the test database from
zc_install/sql/install/mysql_zencart.sql,mysql_utf8.sqlandsql/demo/mysql_demo.sql, and then runs theInitialSetupSeeder. Every test class therefore starts from the same demo-data store, and thezc_installdirectory must be present. - The web server that serves
HTTP_SERVERand the PHPUnit process both need to read the same database settings. When the web server receives a request from the test browser (or is started with the environment variableZENCART_TESTFRAMEWORK_RUNNING=1), Zen Cart loads a test configure file in place ofincludes/configure.php.
Initial setup
- Run everything from the command line. On Windows use WSL2. On macOS and Linux use the normal terminal.
- Use a separate clone of the Zen Cart repository for testing, at the branch you want to test.
- Create an empty database for testing. The database user needs permission to drop and create tables in it.
- Have a web server serving the clone at a URL you will set as
HTTP_SERVERin the configure files below. Two common choices:- DDEV: the framework recognises a DDEV project automatically. Its example configure files use
https://zencart.ddev.site, database hostdb, and user, password and database name ofroot,rootanddb. - PHP’s built-in server, which is what the GitHub Actions workflows use (see Using the PHP built-in server).
- DDEV: the framework recognises a DDEV project automatically. Its example configure files use
Installing the test tools
-
Install Composer.
-
From the root of the clone, run:
composer installThis installs PHPUnit, Symfony BrowserKit, HttpClient and the other test-only dependencies from
composer.json. -
If your PHP version is 8.4 or newer and the install fails, or tests fail with errors from inside
vendor, runcomposer updateto move the test tools to newer versions. Running it again from time to time is also a reasonable way of picking up updates.
Running the tests
| Command | Runs |
|---|---|
composer unit-tests |
The Unit suite only |
composer feature-tests |
FeatureStore and FeatureAdmin together |
composer feature-tests-store |
Storefront feature tests |
composer feature-tests-admin |
Admin feature tests |
Each is a shortcut in composer.json for a phpunit --testsuite ... command. You can also call PHPUnit directly, which is useful for running one test class or method:
vendor/bin/phpunit --testsuite FeatureStore --filter CreateAccountTest
vendor/bin/phpunit --testsuite Unit --process-isolation --filter DiscountCouponsTest
Logs written by the application during a test run are copied to not_for_release/testFramework/logs/console/store/ or .../admin/, which is also what the CI jobs upload as artifacts when a run fails.
Unit tests
Unit tests live in not_for_release/testFramework/Unit/, in a directory per area (for example testsCategories/ and testsNotifiers/). Test files must end in Test.php.
Extend Tests\Support\zcUnitTestCase. Its setUp() defines the constants and loads the files a test needs to run Zen Cart classes on their own (paths, database_tables.php, filenames.php, a notifier instance and the polyfills), so call parent::setUp() first. It also provides mockIterator() and assertURLGenerated().
namespace Tests\Unit\testsExample;
use Tests\Support\zcUnitTestCase;
class ExampleTest extends zcUnitTestCase
{
public function setUp(): void
{
parent::setUp();
require_once DIR_FS_CATALOG . DIR_WS_CLASSES . 'category_tree.php';
}
public function testSomething()
{
$this->assertTrue(true);
}
}
No local configuration is needed to run unit tests. The CI job only creates an empty includes/configure.php first.
Feature tests
Configure files
Feature tests need configure files that tell Zen Cart which URL, directory and database to use during a test run. Put them in not_for_release/testFramework/Support/configs/. They are ignored by git.
You need one file for each of these contexts:
| File | Used by |
|---|---|
<name>.store.configure.php |
The storefront, and FeatureStore tests |
<name>.admin.configure.php |
The admin, and FeatureAdmin tests |
<name>.main.configure.php |
The test runner itself (optional, see Mail server) |
Copy store.configure.php.example and admin.configure.php.example as starting points. They have the same format as the normal Zen Cart configure.php files. Set at least HTTP_SERVER (and HTTPS_SERVER for the store), DIR_FS_CATALOG and the DB_* settings. The test database is specified here. The store and admin files must use the same database.
Choosing <name>
-
On the
2.2branch the framework looks for files for these names, in this order: your login user, thenddev, thenrunner. Under DDEV the user is alwaysddev. To see which user the framework detects:php not_for_release/testFramework/detectUser.php -
For each name, it first looks for a file specific to the branch you are on, and then for the generic one. For example, on
2.2it triesddev.2.2.store.configure.phpand thenddev.store.configure.php. This lets one working copy keep separate settings for several branches. The branch is taken from the environment variablesZENCART_TESTFRAMEWORK_BRANCH_FAMILY,ZENCART_TESTFRAMEWORK_BASE_BRANCHorGITHUB_BASE_REF, otherwise from your checked-out branch if it ismasteror2.2, otherwise from whichever of those two is nearest in the git history. SetZENCART_TESTFRAMEWORK_BRANCH_CANDIDATES(defaultmaster,2.2) to change the branches considered. -
The
runner.*.configure.phpfiles are committed to the repository for GitHub Actions. They use127.0.0.1for the database andHTTP_SERVERfrom the environment, butDIR_FS_CATALOGis a GitHub runner path. Do not use them locally without changing that. Create your own files instead. -
On the
2.1branch there is no fallback and no branch matching. The files must be named exactly<user>.store.configure.phpand so on, where<user>is the value fromdetectUser.php(ddevunder DDEV).
If no matching file is found the tests stop with a message naming the directory they searched.
Using the PHP built-in server
You do not need Apache or DDEV. In one terminal, start the server from the root of the clone:
HTTP_SERVER=http://localhost:8000 \
ZENCART_TESTFRAMEWORK_RUNNING=1 \
ZENCART_TESTFRAMEWORK_CONFIG_USER=<name> \
php -d auto_prepend_file="$PWD/not_for_release/testFramework/Support/php_server_prepend.php" \
-S localhost:8000 -t .
where <name> is the prefix of your configure files. In another terminal run the tests with HTTP_SERVER=http://localhost:8000 set in the environment. This is the same sequence as in .github/workflows/zc_feature_test_store_suite.yml, which you can copy from.
Writing a feature test
Feature tests live in not_for_release/testFramework/FeatureStore/ and FeatureAdmin/. Extend the matching base class:
Tests\Support\zcFeatureTestCaseStoreTests\Support\zcFeatureTestCaseAdmin
They provide $this->browser (a Symfony HttpBrowser with cookie support), and helper methods from these traits, which they already use:
| Trait | Helpers |
|---|---|
GeneralConcerns |
buildStoreLink($page), buildAdminLink($cmd), browserAdminLogin(), locateElementInPageSource(), and the plugin file helpers installPluginToFilesystem() and removePlugin() |
CustomerAccountConcerns |
createCustomerAccountOrLogin($profile), loginCustomer(), logoutCustomer(), gift voucher and group-discount setup |
ConfigurationSettingsConcerns |
setConfiguration($key, $value), getConfigurationSetting($key), and switches for tax-inclusive pricing and shipping tax |
DiscountCouponConcerns |
createCoupon($profile) |
LowOrderFeeConcerns |
switchLowOrderFee($mode) |
LogFileConcerns |
logFilesExists() |
Customer and admin details come from named profiles in Support/helpers/ProfileManager.php, such as florida-basic1 or admin. The admin login is Admin / password.
For direct database checks use Tests\Support\Database\TestDb (selectOne(), selectValue(), insert(), update(), truncate()). It uses prepared statements and is available on 2.2. On 2.1, tests query the database through Eloquent models (for example App\Models\Customer) instead.
namespace Tests\FeatureStore\CreateAccount;
use Tests\Support\Database\TestDb;
use Tests\Support\Traits\CustomerAccountConcerns;
use Tests\Support\zcFeatureTestCaseStore;
class CreateAccountTest extends zcFeatureTestCaseStore
{
use CustomerAccountConcerns;
public function testCreateAccount()
{
$profile = $this->createCustomerAccountOrLogin('florida-basic1');
$customer = TestDb::selectOne(
'SELECT customers_firstname FROM customers WHERE customers_email_address = :email LIMIT 1',
[':email' => $profile['email_address']]
);
$this->assertSame($profile['firstname'], $customer['customers_firstname']);
}
}
To check a page directly, use the browser:
$this->browser->request('GET', $this->buildStoreLink('shopping_cart'));
$this->assertEquals(200, $this->browser->getResponse()->getStatusCode());
Seeders
Custom seeders add or change database rows for one test. They live in not_for_release/testFramework/Support/database/Seeders/ in the Seeders namespace, with a run() method. Run one from a test:
$this->runCustomSeeder('StoreWizardSeeder');
Replace StoreWizardSeeder with the name of your class. Existing seeders include StoreWizardSeeder (store name and owner), CouponTableSeeder, CompoundTaxesSeeder and DisplayLogsSeeder.
namespace Seeders;
use Tests\Support\Database\TestDb;
class MySeeder
{
public function run()
{
TestDb::update(
'configuration',
['configuration_value' => 'My Test Store'],
'configuration_key = :config_key',
[':config_key' => 'STORE_NAME']
);
}
}
Mail server emulation
By default the framework disables sending of email. A main configure file can switch on an SMTP test server, for example Mailpit (included with DDEV). It returns an array:
<?php
return [
'use-server' => true,
'use-mailserver' => true,
'mailserver-host' => 'localhost',
'mailserver-port' => 1025,
'mailserver-user' => 'ddev',
'mailserver-password' => 'mailpit',
];
Save it as <name>.main.configure.php. The committed runner.main.configure.php shows the same keys with use-mailserver set to false. In the 2.2 set-up code both use-server and use-mailserver must be present and true, which is why the example sets both. A wrong setting here usually shows up as failing tests. Avoid pointing it at a real mail server, since repeated similar test messages may get flagged as spam.
Plugin tests
FeatureAdmin/PluginTests/ shows how to test plugin installation. The sample plugin used is in Support/plugins/zenTestPlugin/. A test copies it into zc_plugins/ with installPluginToFilesystem() and removes it with removePlugin().
Running in CI
The repository has three GitHub Actions workflows, zc_unit_test_suite.yml, zc_feature_test_store_suite.yml and zc_feature_test_admin_suite.yml. They run against several PHP versions, start MySQL as a service, start the built-in PHP server with the runner profile, and then run the relevant suite. Use them as a reference for setting up your own CI.
Troubleshooting
- “could not find config file” or “No test config file found”: there is no configure file for your user, branch and context. Check the name against the list in Configure files.
- Feature tests fail straight away with connection errors:
HTTP_SERVERin your store and admin configure files is not reachable from where PHPUnit runs, or the web server is not running. - Database errors during set-up: the database user cannot drop or create tables, or the store and admin configure files use different databases.
- Pages work but data is missing: the framework rebuilds the database for each test class. A test that needs extra data should set it up itself, with a seeder or with
TestDb.