Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: CI

on:
push:
branches:
- 2.next-cake4
- 3.next-cake5

pull_request:
branches:
Expand Down Expand Up @@ -52,8 +55,11 @@ jobs:
- name: Run PHPUnit
run: vendor/bin/phpunit

cs-stan:
name: Coding Standard & Static Analysis
- name: Run integration tests
run: composer test-integration

cs:
name: Coding Standard
runs-on: ubuntu-24.04

steps:
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Changelog
=========

Releases for CakePHP 5.0
------------------------
* 3.1.0
* **Breaking (behaviour)** — the per-test transaction now opens before `setUp()` runs, so data written in `setUp()` (fixtures, factories, `save()`) is rolled back with the test instead of being committed. Tests that relied on `setUp()` data carrying over to later tests in the same class (for example, seeding once behind a static flag) must seed per test.
* **Breaking (API)** — `PHPUnitBeforeTestMethodCalledSubscriber` was removed; register `PHPUnitPreparationStartedSubscriber` instead. Applications that registered the old subscriber by hand will get a fatal error after upgrading.
* Note — do not combine this extension with tests that declare `$fixtures`: CakePHP's `TransactionStrategy` requires no active transaction, and the per-test transaction is already open when `setUp()` runs. Use one strategy or the other.
* Fix: the transaction is opened before `setUp()` (`PreparationStarted`), so `setUp()` data no longer leaks into the next test
* Fix: the per-test transaction is rolled back with `rollback(true)` consistently. Prepared tests are closed on `Test\Finished`; tests that never reach it (unmet requirements, or a skip/error during `setUp()`) are closed by the next test's `PreparationStarted` or by `TestRunner\Finished` at the end of the run. In that case a `tearDownAfterClass()` that runs in between is rolled back with the still-open transaction
* New: SQLite integration suite (`composer test-integration`) proving `setUp()` data does not survive between tests
* New public API: `PHPUnitPreparationStartedSubscriber`, `PHPUnitTestFinishedSubscriber`, `PHPUnitTestRunnerFinishedSubscriber` and `TransactionGuard`, plus the `TestConnectionTrait` helper; `PHPUnitAfterTestMethodFinishedSubscriber` is kept for backwards compatibility
* Chore: `TransactionGuard` centralises begin/end, and the unit tests no longer build PHPUnit internals
* CI: PHP 8.1–8.5 matrix, coding standard job, integration suite, and no duplicate runs on PR branches

Previous releases (3.0.x and earlier) predate this changelog. See the Git history and tags.
7 changes: 4 additions & 3 deletions Docs/Documentation/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ Generic cycle of test execution under DbTest
--------------------------------------------

* When test suite starts with DbTest, It loads test database from template database snapshot.
* Before test method starts, DbTest initializes the transaction.
* After test method finishes, DbTest rollbacks transaction.
* Before `setUp()` runs, DbTest opens the transaction.
* After the test finishes, DbTest rolls back the transaction.

This way database modifications are quickly restored, but requires that the table uses a transaction engine
like InnoDb.
like InnoDb. Because the transaction is already open when `setUp()` runs, tests using DbTest must not declare
`$fixtures` (CakePHP's fixture transaction strategy cannot run inside an open transaction).

Support
-------
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"phpstan-baseline": "tools/phpstan --generate-baseline",
"psalm-baseline": "tools/psalm --set-baseline=psalm-baseline.xml",
"stan-setup": "phive install",
"test": "phpunit"
"test": "phpunit",
"test-integration": "phpunit -c phpunit.integration.xml"
},
"config": {
"allow-plugins": {
Expand Down
38 changes: 38 additions & 0 deletions phpunit.integration.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013 - 2023, Cake Development Corporation (https://www.cakedc.com)
~
~ Licensed under The MIT License
~ Redistributions of files must retain the above copyright notice.
~
~ @copyright Copyright 2013 - 2023, Cake Development Corporation (https://www.cakedc.com)
~ @license MIT License (http://www.opensource.org/licenses/mit-license.php)
-->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
colors="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd">
<php>
<ini name="memory_limit" value="-1"/>
<ini name="apc.enable_cli" value="1"/>
</php>

<testsuites>
<testsuite name="DbTest Integration Suite">
<directory>tests/Integration/</directory>
</testsuite>
</testsuites>

<extensions>
<bootstrap class="CakeDC\DbTest\Test\TestSuite\Fixture\SqliteExtension"/>
</extensions>

<source>
<include>
<directory suffix=".php">src/</directory>
</include>
</source>
</phpunit>
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
*/
namespace CakeDC\DbTest\TestSuite\Fixture\Extension;

use Cake\Database\Connection;
use Cake\Datasource\ConnectionManager;
use PHPUnit\Event\Test\AfterTestMethodFinished;
use PHPUnit\Event\Test\AfterTestMethodFinishedSubscriber;

Expand All @@ -22,14 +20,21 @@
*/
class PHPUnitAfterTestMethodFinishedSubscriber implements AfterTestMethodFinishedSubscriber
{
use TestConnectionTrait;

/**
* @param \CakeDC\DbTest\TestSuite\Fixture\Extension\TransactionGuard $guard Transaction guard.
*/
public function __construct(private readonly TransactionGuard $guard = new TransactionGuard())
{
}

/**
* @param \PHPUnit\Event\Test\AfterTestMethodFinished $event Event
* @return void
*/
public function notify(AfterTestMethodFinished $event): void
{
$connection = ConnectionManager::get('test');
assert($connection instanceof Connection);
$connection->rollback();
$this->guard->end($this->testConnection());
}
}
4 changes: 3 additions & 1 deletion src/TestSuite/Fixture/Extension/PHPUnitExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ public function bootstrap(Configuration $configuration, Facade $facade, Paramete
$facade->registerSubscribers(
new PHPUnitStartedSubscriber($fixtureManager),
new PHPUnitPreparationStartedSubscriber(),
new PHPUnitAfterTestMethodFinishedSubscriber()
new PHPUnitAfterTestMethodFinishedSubscriber(),
new PHPUnitTestFinishedSubscriber(),
new PHPUnitTestRunnerFinishedSubscriber()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
*/
namespace CakeDC\DbTest\TestSuite\Fixture\Extension;

use Cake\Database\Connection;
use Cake\Datasource\ConnectionManager;
use PHPUnit\Event\Test\PreparationStarted;
use PHPUnit\Event\Test\PreparationStartedSubscriber;

Expand All @@ -28,18 +26,21 @@
*/
class PHPUnitPreparationStartedSubscriber implements PreparationStartedSubscriber
{
use TestConnectionTrait;

/**
* @param \CakeDC\DbTest\TestSuite\Fixture\Extension\TransactionGuard $guard Transaction guard.
*/
public function __construct(private readonly TransactionGuard $guard = new TransactionGuard())
{
}

/**
* @param \PHPUnit\Event\Test\PreparationStarted $event Event
* @return void
*/
public function notify(PreparationStarted $event): void
{
$connection = ConnectionManager::get('test');
assert($connection instanceof Connection);
// a previous test may have left its transaction open (e.g. unmet requirements)
if ($connection->inTransaction()) {
$connection->rollback(true);
}
$connection->begin();
$this->guard->begin($this->testConnection());
}
}
13 changes: 8 additions & 5 deletions src/TestSuite/Fixture/Extension/PHPUnitStartedSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
*/
class PHPUnitStartedSubscriber implements StartedSubscriber
{
use TestConnectionTrait;

/**
* @var true
*/
Expand All @@ -35,9 +37,11 @@ class PHPUnitStartedSubscriber implements StartedSubscriber

/**
* @param \CakeDC\DbTest\TestSuite\Fixture\FixtureManager $fixtureManager Fixture Manager instance
* @param \CakeDC\DbTest\TestSuite\Fixture\Extension\TransactionGuard $guard Transaction guard.
*/
public function __construct(
private readonly FixtureManager $fixtureManager
private readonly FixtureManager $fixtureManager,
private readonly TransactionGuard $guard = new TransactionGuard()
) {
}

Expand All @@ -47,8 +51,7 @@ public function __construct(
*/
public function notify(Started $event): void
{
$connection = ConnectionManager::get('test');
assert($connection instanceof Connection);
$connection = $this->testConnection();
$database = $connection->config();

try {
Expand All @@ -58,10 +61,10 @@ public function notify(Started $event): void
$this->fixtureManager->setupDatabase($database, true, false);
$ds = ConnectionManager::get('test');
}
assert($ds instanceof Connection);
$ds = $this->asConnection($ds);
// rollback transaction. gives us a 'fresh' copy of the database for the next
// test suite to run
$ds->rollback();
$this->guard->end($ds);

if (!$this->databaseLoaded) {
$this->loadDatabase($ds, $database);
Expand Down
43 changes: 43 additions & 0 deletions src/TestSuite/Fixture/Extension/PHPUnitTestFinishedSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);

/**
* Copyright 2013 - 2023, Cake Development Corporation (https://www.cakedc.com)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2013 - 2023, Cake Development Corporation (https://www.cakedc.com)
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace CakeDC\DbTest\TestSuite\Fixture\Extension;

use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;

/**
* Closes the per-test transaction when the test finished.
*
* AfterTestMethodFinished is not emitted for test classes without after-test
* hooks; Finished is emitted for every prepared test, so it covers that gap.
*/
class PHPUnitTestFinishedSubscriber implements FinishedSubscriber
{
use TestConnectionTrait;

/**
* @param \CakeDC\DbTest\TestSuite\Fixture\Extension\TransactionGuard $guard Transaction guard.
*/
public function __construct(private readonly TransactionGuard $guard = new TransactionGuard())
{
}

/**
* @param \PHPUnit\Event\Test\Finished $event Event
* @return void
*/
public function notify(Finished $event): void
{
$this->guard->end($this->testConnection());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);

/**
* Copyright 2013 - 2023, Cake Development Corporation (https://www.cakedc.com)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2013 - 2023, Cake Development Corporation (https://www.cakedc.com)
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace CakeDC\DbTest\TestSuite\Fixture\Extension;

use PHPUnit\Event\TestRunner\Finished;
use PHPUnit\Event\TestRunner\FinishedSubscriber;

/**
* Final safety net: closes a transaction still open when the run ends.
*
* Covers tests that never reached their after-test hooks (unmet requirements,
* preparation errors) and prevents Connection::__destruct from logging an
* active transaction.
*/
class PHPUnitTestRunnerFinishedSubscriber implements FinishedSubscriber
{
use TestConnectionTrait;

/**
* @param \CakeDC\DbTest\TestSuite\Fixture\Extension\TransactionGuard $guard Transaction guard.
*/
public function __construct(private readonly TransactionGuard $guard = new TransactionGuard())
{
}

/**
* @param \PHPUnit\Event\TestRunner\Finished $event Event
* @return void
*/
public function notify(Finished $event): void
{
$this->guard->end($this->testConnection());
}
}
46 changes: 46 additions & 0 deletions src/TestSuite/Fixture/Extension/TestConnectionTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);

/**
* Copyright 2013 - 2023, Cake Development Corporation (https://www.cakedc.com)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2013 - 2023, Cake Development Corporation (https://www.cakedc.com)
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace CakeDC\DbTest\TestSuite\Fixture\Extension;

use Cake\Database\Connection;
use Cake\Datasource\ConnectionManager;
use RuntimeException;

/**
* Resolves and validates the "test" connection used by the extension.
*/
trait TestConnectionTrait
{
/**
* @return \Cake\Database\Connection
* @throws \RuntimeException When the "test" connection is not a database connection.
*/
protected function testConnection(): Connection
{
return $this->asConnection(ConnectionManager::get('test'));
}

/**
* @param mixed $connection Connection to validate.
* @return \Cake\Database\Connection
* @throws \RuntimeException When the connection is not a database connection.
*/
protected function asConnection(mixed $connection): Connection
{
if (!$connection instanceof Connection) {
throw new RuntimeException('The "test" connection must be a Cake\Database\Connection instance.');
}

return $connection;
}
}
Loading
Loading