Skip to content
...

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.