At the beginning of a test, the so-called test fixture is prepared. For example, if a method is to be tested, an object of the corresponding class is needed. After this setup, the action is triggered, for example a method is called, whose results are to be tested. Finally, it is checked whether the expected results have occurred.
Sometimes the test fixture is more complex than a single simple object. The amount of code needed to prepare the test fixture grows accordingly. If we are not careful, the readability of the test method suffers from the mixing of code for preparing the test fixture with the actual test code. It therefore makes sense to move the code for setting up the test fixture to a separate method.
Test Fixture with setUp()
Since the beginning, PHPUnit provides two template methods setUp()
and tearDown()
to separate code for managing the test fixture from the actual test code:
<?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 ; |
} |
} |
PHPUnit calls the template method setUp()
before each test. After each test, tearDown()
is called. The PHPUnit documentation has this to say:
setUp()
andtearDown()
are nicely symmetrical in theory, but not in practice. In practice, you only need to implementtearDown()
if you have allocated external resources such as files or sockets insetUp()
. If yoursetUp()
just creates plain PHP objects, you can generally ignoretearDown()
.However, if you create many objects in your
setUp()
, you may want tounset()
the variables holding those objects in yourtearDown()
so that they can be garbage collected sooner. Objects created withinsetUp()
(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.
One problem with the setUp()
and tearDown()
template methods is that they are called before and after every test of the test class, respectively. Even for tests that do not use the test inventory 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 ) ; |
namespace example ; |
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 annotations @before
and @after
were introduced long ago. With these, multiple methods can be configured to be called before and after a test, respectively.
Test Fixture without setUp()
I write the test shown at the beginning like this today:
<?php declare ( strict_types = 1 ) ; |
namespace example ; |
use PHPUnit\Framework\TestCase ; |
final class ExampleTest extends TestCase |
{ |
public function testSomething ( ) : void |
{ |
$this -> assertSame ( |
'the-result' , |
$this -> example ( ) -> doSomething ( ) |
) ; |
} |
private function example ( ) : Example |
{ |
return new Example ( |
$this -> createStub ( Collaborator :: class ) |
) ; |
} |
} |
No test-specific state is stored in the test object, for example in a property such as $this->example
. After the execution of the test method is finished, the test no longer holds a reference to the object created in the example()
method. PHP's garbage collector can therefore clean it up automatically.
The example()
method is called only by the tests that need the object created in this way. Furthermore, it is a private implementation detail of the test class, so problems due to inheritance are avoided.
By the way, I was able to declare a type for the return value of the example()
method even before PHP 7.4 finally introduced type declarations for properties.
I think it's these three reasons that have led me to subconsciously change the way I manage test fixture in my tests.
A couple of years ago I wrote in another article :
It is important to keep in mind that best practices for a tool such as PHPUnit are not set in stone. They rather evolve over time and have to be adapted to changes in PHP, for instance.
Back then it was about testing exceptions, now it is about managing test fixtures. I will continue to do the latter without the setUp()
and tearDown()
methods.