1. Homepage
  2. News
  3. 2016
  4. February
  5. Questioning PHPUnit Best Practices

Questioning PHPUnit Best Practices

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. Recently I was involved in a discussion that questioned the current best practice for testing exceptions. That discussion resulted in changes in PHPUnit 5.2 that I would like to explain in this article.

The Original Best Practice: return or fail()

A long time ago, when PHP 5.0 had just been released and PHPUnit 2 was the latest and greatest, the approach shown in the example below was the best practice for testing exceptions:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<?php
class ExampleTest extends PHPUnit_Framework_TestCase
{
public function testExpectedExceptionIsRaised ( )
{
// Arrange
$example = new Example ;

// Act
try {
$example -> doSomething ( ) ;
} catch ( ExpectedException $e ) {
return ;
}

// Assert
$this -> fail ( 'ExpectedException was not raised' ) ;
}
}

Like any other test, the test shown above has three distinct phases: in the arrange phase the object under test is prepared, in the act phase the action to be tested is performed, and in the assert phase the outcome of the action is verified. The best practice for testing exceptions back then was to wrap the code of the act phase into a try block. The return statement in the respective catch block would signal a success to PHPUnit. The call to the fail() would signal a test failure to PHPUnit.

The Current Best Practice: @expectedException

The approach shown in the example above for testing exceptions was inconvenient and often times resulted in hard to read test code. Together with the fact that DocBlock-based annotations were getting popular in the PHP world this lead me to believe that the following would be a better approach:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php
class ExampleTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException ExpectedException
*/
public function testExpectedExceptionIsRaised ( )
{
// Arrange
$example = new Example ;

// Act
$example -> doSomething ( ) ;
}
}

In the example above, the @expectedException annotation tells PHPUnit that this test expects the code under test to raise an exception of a specified type. If that exception is raised then the test is considered a success. If the exception is not raised the test will be considered a failure.

The implementation of the @expectedException annotation uses the setExpectedException() method to set up the expectation. This method can also be used directly, for instance if you prefer expressing the assert phase explicitly in code rather than in a comment. Due to the nature of exceptions the assert phase and act phase are swapped in the test code shown below:

1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php
class ExampleTest extends PHPUnit_Framework_TestCase
{
public function testExpectedExceptionIsRaised ( )
{
// Arrange
$example = new Example ;

// Assert
$this -> setExpectedException ( ExpectedException :: class ) ;

// Act
$example -> doSomething ( ) ;
}

Over time the @expectedExceptionCode, @expectedExceptionMessage, and @expectedExceptionMessageRegExp annotations were added. These allow the configuration of expectations for exception codes and messages. Unfortunately, though, they were not implemented in a clean way, which made the setExpectedException() method less and less convenient to use as an alternative to the annotations.

Ever since I added the @expectedException annotation to PHPUnit I considered using it a best practice. This was reflected in PHPUnit's documentation as well as in my conference presentations and trainings, for instance.

A New Best Practice: expectException()

One morning not too long ago I got a phone call from Stefan Priebsch. He had just discussed the topic of testing exceptions with the students of his master class at the University of Rosenheim. One of his students had used the setExpectedException() method in his homework. When Stefan told him that he should have used the @expectedException annotation, the student challenged that best practice. After discussing the topic over the phone I had to admit that the student was right. While the @expectedException annotation is convenient to use it is also problematic. Lets look at the problems the student pointed out.

Back when the annotation was added to PHPUnit there was no support for namespaces in PHP. These days, though, namespaces are commonly used in PHP code. And since an annotation such as @expectedException is technically only a comment and not part of the code you have to use a fully-qualified class name such as vendor\project\Example when you use it. In a comment you cannot use an unqualified class name, Example for instance, that you would be able to use in code when that class is in or imported into the current namespace.

1 2 3 4 5 6 7 8 9 10 11 12
<?php
namespace vendor \ project ;

class ExampleTest extends \ PHPUnit_Framework_TestCase
{
public function testExpectedExceptionIsRaised ( )
{
$this -> expectException ( ExpectedException :: class ) ;

// ...
}
}

In the example above, we use the expectException() method that was introduced in PHPUnit 5.2 to tell PHPUnit that the test expects an exception of a specified type to be raised. Thanks to the class constant that holds the fully-qualified name of a class we do not need to write vendor\project\ExpectedException in the test code, but we can write ExpectedException::class instead. This improves the readability of the test and makes automated refactorings in modern IDEs such as PhpStorm reliable.

Another advantage of setting up the expectation in the test code has to do with the three phases of a test we discussed earlier. When we use the @expectedException annotation then PHPUnit will consider the test successful if the exception is raised at any point in time during the execution of the test method. This may not always be what you want:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<?php
namespace vendor \ project ;

class ExampleTest extends \ PHPUnit_Framework_TestCase
{
public function testExpectedExceptionIsRaised ( )
{
// Arrange
$example = new Example ;

// Assert
$this -> expectException ( ExpectedException :: class ) ;

// Act
$example -> foo ( ) ;
}
}

PHPUnit will consider the test shown in the example above then and only then a success when the expected exception is raised after the call to the expectException() method. If the constructor of the Example class raises an exception of the same type then this will be considered an error by PHPUnit.

In addition to the expectException() method, PHPUnit 5.2 also introduces the expectExceptionCode(), expectExceptionMessage, and expectExceptionMessageRegExp() methods for programmatically setting expectations for exceptions. There is nothing that you can do with these new methods that you could not have done with the old setExpectedException() method. However, because setExpectedException() can do everything these new methods can its API was convoluted and inconvenient. Separation of concerns, anyone? The setExpectedException() method has been deprecated and will be removed in PHPUnit 6.

Hindsight is easier than foresight. I am not sure anymore that adding the @expectedException did more good than harm. I do not currently plan to remove it from PHPUnit, though. But going forward I will recommend using expectException() etc. for testing exceptions. I would like to invite you all to question best practices like this. Nothing can be set in stone if we want to evolve PHP and its ecosystem of tools, frameworks, and libraries.

Training

Professional Object-Oriented Development with PHP

  • Montréal, Canada

In this two-day event, your trainers Sebastian Bergmann, Arne Blankerts, and Stefan Priebsch will explain to you how to successfully develop business logic in a decoupled, tested and extensible way.

read more

Hands-On

Built-In Bytecode Cache

PHP 5.5 ships with a built-in bytecode cache. We provide background information an show how you can easily benefit from improved performance.

read more

Open Source

Open Knowledge

Following our visit to the Wikimedia Foundation headquarters in San Francisco last year we supported the foundation's German branch in Berlin this year.

read more
The community is important for us.