Lifecycle
Lifecycle attributes let you run code before and after tests — for setting up the environment, cleaning up state, and managing resources.
Plugin class: LifecyclePlugin\Testo\Lifecycle\LifecyclePlugin. Included in SuitePlugins\Testo\Application\Config\Plugin\SuitePlugins — enabled by default.
Class Instantiation
By default, Testo instantiates each test class once per test case, not per test. This means:
- Instance properties persist between tests in the same class
- Constructor runs lazily — right before the first non-static method call
- If all methods are static, the class is never instantiated
php
final class ServiceTest
{
private Client $client;
private int $counter = 0;
public function __construct()
{
// Runs once — natural place for expensive initialization
$this->client = new Client();
}
#[Test]
public function firstTest(): void
{
$this->counter++;
// $this->counter is now 1
}
#[Test]
public function secondTest(): void
{
$this->counter++;
// $this->counter is now 2 — state persists between tests
// $this->client is still the same instance
}
}To control state between tests, use lifecycle attributes described below.
Attributes
#[BeforeTest]
#[BeforeTest(int $priority = 0)]Runs a method before each test in the class.$priority -> Execution priority. Higher values run first.#[AfterTest]
#[AfterTest(int $priority = 0)]Runs a method after each test in the class.$priority -> Execution priority. Higher values run first.#[BeforeClass]
#[BeforeClass(int $priority = 0)]Runs a method once before all tests in the class. Suitable for expensive setup.$priority -> Execution priority. Higher values run first.#[AfterClass]
#[AfterClass(int $priority = 0)]Runs a method once after all tests in the class. Suitable for cleanup.$priority -> Execution priority. Higher values run first.Execution Order
BeforeClass (once)
├── BeforeTest
│ └── Test 1
│ └── AfterTest
├── BeforeTest
│ └── Test 2
│ └── AfterTest
└── ...
AfterClass (once)Basic Example
php
final class DatabaseTest
{
private static Connection $connection;
private Transaction $transaction;
#[BeforeClass]
public static function connect(): void
{
self::$connection = new Connection();
}
#[AfterClass]
public static function disconnect(): void
{
self::$connection->close();
}
#[BeforeTest]
public function beginTransaction(): void
{
$this->transaction = self::$connection->beginTransaction();
}
#[AfterTest]
public function rollback(): void
{
$this->transaction->rollback();
}
#[Test]
public function insertsRecord(): void
{
self::$connection->insert('users', ['name' => 'John']);
Assert::same(1, self::$connection->count('users'));
}
}Priority
When you have multiple methods with the same lifecycle attribute, use priority to control execution order:
php
#[BeforeTest(priority: 100)]
public function initializeConfig(): void
{
// Runs first (highest priority)
}
#[BeforeTest(priority: 50)]
public function initializeLogger(): void
{
// Runs second
}
#[BeforeTest] // priority: 0 (default)
public function initializeService(): void
{
// Runs last
}Higher values execute first. Default priority is 0.