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.
<?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.
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:
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
abstract class MyTestCase extends TestCase
{
protected function setUp(): void
{
// ...
}
}
<?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, the attributes
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.
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.
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 usingglobal $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).