Skip to content

Setup ​

Using a different PHP framework? ​

This plugin is built on top of CakePHP's ORM (cakephp/orm) — the produced objects are CakePHP entities, marshalled by Table::patchEntity(), with associations expressed in Cake's shape. If your project is built on a different framework, prefer the factory library that is canonical for that ecosystem instead of bringing in a second ORM:

  • Laravel → built-in Eloquent factories (Illuminate\Database\Eloquent\Factories\Factory) paired with the RefreshDatabase trait.
  • Symfony / Doctrine (ORM or ODM) → zenstruck/foundry.

The section below covers the rarer case of using this plugin with a standalone cakephp/orm install — i.e. you're using Cake's ORM as a library outside the full Cake application stack, not Laravel/Symfony interop.

Standalone cakephp/orm (no full Cake app) ​

For applications that use cakephp/orm directly (without the rest of the Cake framework), use whatever method your test stack provides for managing the database, or opt for the universal test-database-cleaner.

Only the factories work in this mode. The bake and persist commands and the test suite strategies need the full cakephp/cakephp package.

Define your DB connections in your test bootstrap.php as described in the CakePHP cookbook.

See also: Associations for standalone cakephp/orm.

CakePHP apps ​

Follow Installation to install and load the plugin, choose a generator and prepare a separate test database.

We recommend using migrations for managing the schema of your test DB with the CakePHP Migrations plugin.

Cleaning data between tests ​

Test data must be cleaned between tests to avoid entity collisions and unexpected results. CakePHP ships with Fixture State Managers and provides the TruncateStrategy (truncate all tables after each test run) as well as the TransactionStrategy (create a transaction and roll it back after each test run).

For fixture-factory–driven test suites, this plugin provides a dedicated strategy that pairs cleanly with factories — see below.

This plugin provides the FactoryTransactionStrategy which automatically:

  • Wraps all database operations in transactions
  • Rolls back after each test (both factory and application data)
  • Resets unique generator state (fixes OverflowException issues)
  • Tracks which tables are written to by fixture factories

Unlike the standard TransactionStrategy, this doesn't require manually listing fixtures — in fact, the strategy works best if you don't use classic $fixtures arrays at all.

CakePHP 5.2+ (global configuration) ​

In CakePHP 5.2+, configure the fixture strategy globally in config/app.php:

php
'TestSuite' => [
    'fixtureStrategy' => \CakephpFixtureFactories\TestSuite\FactoryTransactionStrategy::class,
],

This applies the strategy to all test cases automatically. No traits needed.

Tip: See config/app.example.php in this plugin for a full list of available configuration options, including generator type, seed, and instance-level generator management.

CakePHP 5.0–5.1 (trait-based) ​

For older CakePHP versions, use FactoryTransactionTrait. Two patterns:

php
namespace App\Test;

use Cake\TestSuite\TestCase;
use CakephpFixtureFactories\TestSuite\FactoryTransactionTrait;

abstract class AppTestCase extends TestCase
{
    use FactoryTransactionTrait;
}

// Then extend AppTestCase in every test:
class MyTest extends AppTestCase
{
    // Strategy is automatically applied.
}
php
use CakephpFixtureFactories\TestSuite\FactoryTransactionTrait;

class MyTest extends TestCase
{
    use FactoryTransactionTrait;

    public function testSomething(): void
    {
        $article = ArticleFactory::new()->save();
        $this->Articles->save($article);
        // All data rolled back; generator state reset.
    }
}

Prefer the shared AppTestCase route — the per-test trait pattern works too but means repeating yourself.

Benefits ​

  • No need to manually list $fixtures
  • All data is rolled back - factory data AND application code modifications
  • Faster than truncation strategies
  • Solves unique generator state accumulation - the strategy resets generator state after each test, preventing OverflowException when using unique() modifiers
  • Works seamlessly with nested associations

Note: Table tracking only captures tables written via the factory save methods. The transaction rollback still handles all data modifications regardless of source (factories, controllers, models, etc.) on the primary test connection (eager-wrapped at setup) plus any additional connections a Factory has persisted on during the test (tracked lazily). See "Eager by default" below for the full contract.

Eager by default ​

FactoryTransactionStrategy is eager on the primary test connection: setupTest() opens a transaction on test (configurable via the $primaryConnection property) up-front, so direct table operations during the test ($table->save($entity), $table->delete($entity), raw inserts via $connection->execute(...)) are also rolled back at teardown — not just operations that go through a Factory's save() / saveMany().

Beyond the primary connection, additional connections are still tracked lazily: ensureTransaction() is called from inside BaseFactory::save() / saveMany() the first time a Factory persists on a given connection. Multi-database setups therefore only pay the transaction cost on connections they actually write to.

This covers the standard CakePHP testing patterns out of the box:

php
// Eager-default makes both of these get correctly rolled back:

public function testValidationDefault(): void
{
    $data = ArticleFactory::new()->build()->toArray();
    $article = $this->Articles->newEntity($data);
    $this->assertTrue((bool)$this->Articles->save($article));   // direct table save — covered
}

public function testFind(): void
{
    ArticleFactory::new()->save();                              // Factory persist — covered
    $this->assertNotNull($this->Articles->find()->first());
}

Opting out ​

If your suite persists exclusively through Factories and you want to skip the eager begin (multi-connection optimization, performance under heavy parallel test runs):

Whole suite ​

Point 'fixtureStrategy' at the lazy variant:

php
'TestSuite' => [
    'fixtureStrategy' => \CakephpFixtureFactories\TestSuite\LazyFactoryTransactionStrategy::class,
],

A connection only joins the rollback set when a Factory persists on it.

Single test class ​

Use LazyTransactionTrait on just the affected class. The rest of the suite stays eager:

php
use CakephpFixtureFactories\TestSuite\LazyTransactionTrait;

class HeavyConnectionTest extends \Cake\TestSuite\TestCase
{
    use LazyTransactionTrait;

    // Release the eager transaction before application setUp() seeds data.
    // Tests in this class run with the lazy contract.
}

If your project's primary test connection is named something other than test, override the $primaryConnection property in a FactoryTransactionStrategy subclass:

php
final class MyEagerStrategy extends \CakephpFixtureFactories\TestSuite\FactoryTransactionStrategy
{
    protected string $primaryConnection = 'test_main';
}

Trigger Strategy ​

TriggerStrategy cleans up without a wrapping transaction. On first use it installs an AFTER INSERT trigger on every table of the test connection and every test_* connection. The triggers record the touched tables in a collector table. At the start of each test, only those tables are truncated. Nothing happens at teardown.

Use it when a test cannot run inside one transaction:

  • the code under test commits, rolls back or opens its own transactions
  • the test writes through more than one connection or process
  • the test runs DDL, which commits implicitly on MySQL
  • you want to inspect the test database after a failing test, since its data stays until the next test starts

When none of these apply, FactoryTransactionStrategy is faster: a rollback costs less than truncating tables.

php
'TestSuite' => [
    'fixtureStrategy' => \CakephpFixtureFactories\TestSuite\TriggerStrategy::class,
],

On CakePHP 5.0-5.1, use the CakephpFixtureFactories\TestSuite\TruncateDirtyTables trait in your base test case instead.

Supported drivers: MySQL, Postgres and SQLite. The test database user needs permission to create triggers (and procedures, on MySQL).

Notes ​

  • Migration bookkeeping tables (*phinxlog*, cake_migrations, cake_seeds) get no trigger.
  • A table created after the triggers were installed has no trigger, so it is not cleaned. Run your migrations before the first test, which is the usual Migrator setup.

The strategy reads its settings from the FixtureFactories config key:

php
'FixtureFactories' => [
    'ignoredConnections' => ['test_other'],
    'collectorMode' => 'TEMP', // Or key by connection name, e.g. ['test_other' => 'TEMP']
    'tableSniffer' => MySniffer::class,
],

The legacy TestSuiteLightIgnoredConnections, skipInTestSuiteLight, dirtyTableCollectorMode, and per-connection tableSniffer keys are deprecated but still read. For collectorMode and tableSniffer, a connection-specific entry takes precedence over a plain string, then the legacy connection setting, then the default. Ignored connections from both Configure keys are combined.

Migrating from vierge-noire/cakephp-test-suite-light ​

The strategy is a port of CakephpTestSuiteLight\Fixture\TriggerStrategy. The collector is now fixture_factories_dirty_tables. On first use, an existing test-suite-light database is migrated automatically: the old test_suite_light_dirty_tables table and its triggers are dropped, and the triggers are installed again for the new collector. Swap the namespaces and remove the package:

BeforeAfter
CakephpTestSuiteLight\Fixture\TriggerStrategyCakephpFixtureFactories\TestSuite\TriggerStrategy
CakephpTestSuiteLight\Fixture\TruncateDirtyTablesCakephpFixtureFactories\TestSuite\TruncateDirtyTables
CakephpTestSuiteLight\Sniffer\*CakephpFixtureFactories\TestSuite\Sniffer\*

If you call sniffer methods directly, these names changed. The old ones still work but are deprecated:

DeprecatedUse
activateMainMode() / isInMainMode()activatePermanentMode() / isInPermanentMode()
activateTempMode() / isInTempMode()activateTemporaryMode() / isInTemporaryMode()
getAllTablesExceptPhinxlogs() / getAllTablesExceptPhinxlogsAndCollector()getScopedTables()

Released under the MIT License.