Why I manage test fixture differently

Why I manage test fixture differently

Some time ago I noticed that I no longer use setUp() methods when I write tests. This "just happened", there was no conscious decision behind it. This tweet from Marco Pivetta motivated me to reflect on this change and write down my thoughts in this article.

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() 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(). If your setUp() just creates plain PHP objects, you can generally ignore tearDown().

However, if you create many objects in your setUp(), 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.

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.