5. Fixtures

A test usually follows the “Arrange, Act, Assert” structure: arranging all necessary preconditions and inputs (the so-called test fixture), acting on the object under test, and asserting that the expected results have occurred.

Arrange, Expect, Act

When you expect an action to raise an exception or when you verify the communication between collaborating objects using mock objects then the test usually follows the “Arrange, Expect, Act” structure.

Sometimes the test fixture is made up of a single object, sometimes it is a more complex object graph, for instance. The amount of code needed to set it up will grow accordingly. The actual content of the test gets lost in the noise of setting up the test fixture. This problem gets even worse when you write several tests with similar test fixtures.

PHPUnit supports the reuse of setup code between tests. Before a test method is run, a template method named setUp() is invoked: this is where you can create your test fixture. Once the test method has finished running, whether it succeeded or failed, another template method named tearDown() is invoked: this is where you can clean up the objects against which you tested.

Example 5.1 Example of a test class that uses setUp() and tearDown()
<?php declare(strict_types=1);
namespace example;

use PHPUnit\Framework\TestCase;

final class ExampleTest extends TestCase
{
    private ?Example $example;

    public function testSomething(): void
    {
        $this->assertSame(
            'the-result',
            $this->example->doSomething()
        );
    }

    protected function setUp(): void
    {
        $this->example = new Example(
            $this->createStub(Collaborator::class)
        );
    }

    protected function tearDown(): void
    {
        $this->example = null;
    }
}

The setUp() and tearDown() template methods are run once for each test method (and on fresh instances) of the test case class.

Template Methods

setUp() and tearDown() are actually two of six template methods that PHPUnit calls during the lifecycle of a test case class. Here is the complete execution order:

setUpBeforeClass()          Once before the first test of the class
├── setUp()                 Before each test
│   ├── assertPreConditions()
│   │   ├── test method
│   │   assertPostConditions()
│   tearDown()              After each test
tearDownAfterClass()        Once after the last test of the class

setUpBeforeClass() and tearDownAfterClass() are called once for the entire test case class (see Sharing Fixture). The other four template methods are called for each test method. assertPreConditions() and assertPostConditions() are explained below.

For each of these template methods, an equivalent attribute is available that allows you to configure multiple methods for the same phase, with optional priority ordering. See Using Attributes Instead of Template Methods for details.

One problem with the setUp() and tearDown() template methods is that they are called even for tests that do not use the test fixture managed by these methods, in the example shown above the $this->example property.

Another problem can occur when inheritance comes into play:

Example 5.2 Example of an abstract test case class with a setUp() method
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

abstract class MyTestCase extends TestCase
{
    protected function setUp(): void
    {
        // ...
    }
}
Example 5.3 Example of a concrete test case class that extends an abstract test case class with a setUp() method
<?php declare(strict_types=1);
namespace example;

use PHPUnit\Framework\TestCase;

final class ExampleTest extends MyTestCase
{
    protected function setUp(): void
    {
        // ...
    }
}

If we forget to call parent::setUp() when implementing ExampleTest::setUp(), the functionality provided by MyTestCase will not work. To reduce this risk, attributes such as PHPUnit\Framework\Attributes\Before and PHPUnit\Framework\Attributes\After are available. With these, multiple methods can be configured to be called before and after a test, respectively, without having to call parent::setUp(). See Using Attributes Instead of Template Methods for details and examples.

More setUp() than tearDown()

setUp() and tearDown() are nicely symmetrical in theory, but not in practice. In practice, you only need to implement tearDown() if you have allocated external resources such as files or sockets in setUp(). Unless you create large object graphs in your setUp() and store them in properties of the test object, you can generally ignore tearDown().

However, if you create large object graphs in your setUp() and store them in properties of the test object, you may want to unset() the variables holding those objects in your tearDown() so that they can be garbage collected sooner.

Objects created within setUp() (or test methods) that are stored in properties of the test object are only automatically garbage collected at the end of the PHP process that runs PHPUnit.

Pre-Conditions and Post-Conditions

The assertPreConditions() template method is called after setUp() but before the test method. The assertPostConditions() template method is called after the test method but before tearDown().

These methods are intended for assertions that are shared by all tests of a test case class. Unlike setUp() and tearDown(), a failure in assertPreConditions() or assertPostConditions() is reported as a test failure (not as an error), which means that the failure message will include assertion details.

Example 5.4 Using assertPreConditions() and assertPostConditions()
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class SomeTest extends TestCase
{
    private ?SomeObject $object;

    protected function setUp(): void
    {
        $this->object = new SomeObject;
    }

    protected function assertPreConditions(): void
    {
        $this->assertTrue($this->object->isReady());
    }

    protected function assertPostConditions(): void
    {
        $this->assertFalse($this->object->hasErrors());
    }

    protected function tearDown(): void
    {
        $this->object = null;
    }

    public function testSomething(): void
    {
        $this->object->doSomething();

        $this->assertSame('expected', $this->object->result());
    }
}

In the example above, assertPreConditions() verifies the fixture is in a valid state before each test, and assertPostConditions() verifies that no errors occurred after each test. These checks apply to all tests in the class without having to repeat them in each test method.

Using Attributes Instead of Template Methods

For each of the six template methods, PHPUnit provides a corresponding attribute that can be used to mark methods that should be called in the same phase:

Template Method

Attribute

setUpBeforeClass()

BeforeClass

setUp()

Before

assertPreConditions()

PreCondition

assertPostConditions()

PostCondition

tearDown()

After

tearDownAfterClass()

AfterClass

The key advantage of using attributes over template methods is that multiple methods can be configured for the same phase. This is especially useful in class hierarchies: each class can declare its own hook methods without having to call parent::setUp(), for example.

Example 5.5 Using #[Before] and #[After] attributes
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\TestCase;

abstract class MyTestCase extends TestCase
{
    #[Before]
    protected function setUpLogger(): void
    {
        // Set up logging for all tests ...
    }

    #[After]
    protected function tearDownLogger(): void
    {
        // Tear down logging after all tests ...
    }
}
Example 5.6 Subclass does not need to call parent::setUp()
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Before;

final class MyTest extends MyTestCase
{
    #[Before]
    protected function setUpFixture(): void
    {
        // Set up the fixture for this test class ...
    }

    public function testSomething(): void
    {
        // Both setUpLogger() and setUpFixture() have been called at this point
        // ...
    }
}

When a test case class has more than one method configured with the same attribute, the test runner assumes that the invocation order does not matter. If the order does matter, the attribute’s optional $priority argument can be used to control it: a method with a higher $priority value is invoked before a method with a lower $priority value.

Example 5.7 Controlling execution order with priorities
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\TestCase;

final class PriorityTest extends TestCase
{
    #[Before(priority: 2)]
    protected function connectToDatabase(): void
    {
        // This runs first (higher priority) ...
    }

    #[Before(priority: 1)]
    protected function seedDatabase(): void
    {
        // This runs second (lower priority) ...
    }
}

Sharing Fixture

There are few good reasons to share fixtures between tests, but in most cases the need to share a fixture between tests stems from an unresolved design problem.

A good example of a fixture that makes sense to share across several tests is a database connection: you log into the database once and reuse the database connection instead of creating a new connection for each test. This makes your tests run faster.

The setUpBeforeClass() and tearDownAfterClass() template methods are called before the first test of the test case class is run and after the last test of the test case class is run, respectively.

Do not use assertions in setUpBeforeClass() or tearDownAfterClass()

Please note that using assertions in setUpBeforeClass() or tearDownAfterClass() methods leads to undefined behavior. The only reason why using assertions in these template methods does not lead to an error being emitted by the test runner is that we want to avoid the runtime overhead of determining whether an assertion method is called from these template methods.

Example 5.8 uses the setUpBeforeClass() and tearDownAfterClass() template methods to connect to the database before the test case class’ first test and to disconnect from the database after the last test of the test case, respectively.

Example 5.8 Sharing fixture between the tests of a test suite
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DatabaseTest extends TestCase
{
    private static $dbh;

    public static function setUpBeforeClass(): void
    {
        self::$dbh = new PDO('sqlite::memory:');
    }

    public static function tearDownAfterClass(): void
    {
        self::$dbh = null;
    }
}

The BeforeClass and AfterClass attributes provide the same functionality as setUpBeforeClass() and tearDownAfterClass() but allow multiple methods to be configured for the same phase, with optional priority ordering (see Using Attributes Instead of Template Methods).

It cannot be emphasized enough that sharing fixtures between tests reduces the value of the tests. The underlying design problem is that objects are not loosely coupled. You will achieve better results solving the underlying design problem and then writing tests using stubs (see Test Doubles), than by creating dependencies between tests at runtime and ignoring the opportunity to improve your design.

Global State

It is hard to test code that uses singletons. The same is true for code that uses global variables. Typically, the code you want to test is coupled strongly with a global variable and you cannot control its creation. An additional problem is the fact that one test’s change to a global variable might break another test.

In PHP, global variables work like this:

  • A global variable $foo = 'bar'; is stored as $GLOBALS['foo'] = 'bar';.

  • The $GLOBALS variable is a so-called super-global variable.

  • Super-global variables are built-in variables that are always available in all scopes.

  • In the scope of a function or method, you may access the global variable $foo by either directly accessing $GLOBALS['foo'] or by using global $foo; to create a local variable with a reference to the global variable.

Besides global variables, static properties of classes are also part of the global state.

PHPUnit can optionally run your tests in a way where changes to global and super-global variables ($GLOBALS, $_ENV, $_POST, $_GET, $_COOKIE, $_SERVER, $_FILES, $_REQUEST) do not affect other tests. You can activate this behaviour by using the --globals-backup option or by setting backupGlobals="true" in the XML configuration file.

By using the --static-backup option or setting backupStaticProperties="true" in the XML configuration file, this isolation can be extended to static properties of classes.

Note

The backup and restore operations for global variables and static class properties use serialize() and unserialize().

Objects of some classes (e.g., PDO) cannot be serialized and the backup operation will break when such an object is stored e.g. in the $GLOBALS array.

The PHPUnit\Framework\Attributes\BackupGlobals attribute can be used to control the backup and restore operations for global variables.

The PHPUnit\Framework\Attributes\ExcludeGlobalVariableFromBackup attribute can be used to exclude specific global variables from the backup and restore operations for global variables.

The PHPUnit\Framework\Attributes\BackupStaticProperties attribute can be used to control the backup and restore operations for static properties of classes. This affects all static properties in all declared classes before each test and restore them afterwards. All classes that are declared at the time a test starts are processed, not only the test class itself. It only applies to static class properties, not static variables within functions.

The PHPUnit\Framework\Attributes\ExcludeStaticPropertyFromBackup attribute can be used to exclude specific static properties from the backup and restore operations for static properties.

Note

The backup operation for static properties of classes is performed before a test method, but only if it is enabled. If a static value was changed by a previously executed test that did not have BackupStaticProperties(true), then that value will be backed up and restored — not the originally declared default value.

The same applies to static properties of classes that were newly loaded/declared within a test. They cannot be reset to their originally declared default value after the test, since that value is unknown. Whichever value is set will leak into subsequent tests.

For unit tests, it is recommended to explicitly reset the values of static properties under test in your setUp() code instead (and ideally also tearDown(), so as to not affect subsequently executed tests).

The WithEnvironmentVariable attribute can be used to set or remove environment variables for the duration of a test. Environment variables are set before setUp() is called and restored to their original values after tearDown() has completed.