Using Traits With PHPUnit
First published at Tuesday 29 November 2016
Warning: This blog post is more then 8 years old – read and use with care.
Using Traits With PHPUnit
As we already wrote that "Code Reuse By Inheritance" has lots of problems and we consider it a code smell. You should always aim to use Dependency Injection, most likely Constructor Injection. But with test cases in PHPUnit we cannot do this because we have no control about how and when our test cases are created. There are a similar problem in other frameworks, like we discussed in "Object Lifecycle Control". We also blogged about traits as a Code Smell, but let me show and explain why they might be fine to use in your test cases.
So in PHPUnit it is common to reuse code by inheritance. Even by now we often create some base class providing common functionality. This works fine and is OK if this really defines an "is a"-relationship. Let's continue with an example from our Page Object test repository. The patterns described here are usually not required for Unit Tests (which you should always also write) but mostly for integration or functional tests.
An Example
The example repository implements functional tests for a website using the Page Object pattern. This means that the tests access a website and assert on its contents, try to fill forms, submit them and click links. Such tests are a useful part of your test mix, but should never be your only tests.
Let's see the options for code reuse we employ in this little test project – and the reasoning behind it. We start with a simple test case:
class LoginTest extends FeatureTest
{
public function testLogInWithWrongPassword()
{
$page = (new Page\Login($this->session))->visit(Page\Login::PATH);
$page->setUser(getenv('USER'));
$page->setPassword('wrongPassword');
$newPage = $page->login();
$this->assertInstanceOf(Page\Login::class, $newPage);
}
// …
}
This test case extends from a base class FeatureTest
to re-use its functionality. The base class uses PHPUnits setUp()
method to setup and start the mink test driver which will act as a browser to access the website. And it provides a default tearDown()
method to reset the browser state again.
In this case the inheritance defines a clear "is a" relationship with the FeatureTest
and it overwrites default methods in the PHPUnit stack which should be overwritten in any feature test. The FeatureTest
again extends an IntegrationTest
which is empty in this example but normally would provide access to the application stack to reset a database, access random services or something similar. We just do not need anything like this in this little test project. Since functional tests can be considered a superset of integration tests this is fine again and provides common functionality which belongs to all tests of this type.
Traits
Let's take a look at a slightly more complex test case now:
class DashboardTest extends FeatureTest
{
use Helper\User;
// …
public function testHasDemoOrganization()
{
$this->logIn();
$page = (new Page\Dashboard($this->session))->visit(Page\Dashboard::PATH);
$organizations = $page->getOrganizations();
$this->assertArrayHasKey('demo', $organizations);
return $organizations['demo'];
}
// …
}
In this case we using the Helper\User
trait to include some functionality – it provides the logIn()
method which is used in the test testHasDemoOrganization()
. Not every feature test might need this aspect and in "normal" software you would provide such helpers through constructor injection. But since we do not have any control on the test case creation we include the code using a trait.
The trait enables code reuse – we can use it any test case which requires login. The trait extracts this concern and we do not clutter every test case requiring login with this kind of code.
Whats The Difference?
The trait helps us in this example, the code looks clean, so you might want to ask: Why would traits ever be considered a code smell?
One of the most important reasons is that in a test case there probably won't be a reason to change a dependency without adapting the code (Open Closed Principle). In other words: There is no reason for dynamic dispatch.
A trait establishes a dependency to another class which is defined by the name of the trait (instead of an instance of some class which could be a subtype). There is no easy way to change the actually used implementation from the outside. If you include a LoggerTrait
there is no way to change the used LoggerTrait
, during tests or when the requirements change, without changing code. Traits establish a static
dependency which is hard to mock and hard to replace during runtime or configuration.
But we will never mock our test cases, right? And if the use cases change we will change the test cases. This can happen a lot as compared to unit tests.
Especially (Open Source) libraries and extensible software commonly has the requirement that people should be able to change the behaviour without changing the code. Most likely because they do not have direct access to the code or it would have side effects to other usages of the code. But nobody uses your test cases in such a way, thus you are "allowed" to sin in here – at least a little bit.
And there are no other options. This, generally, can be another reason to use traits. Traits are often an option when refactoring legacy software to temporarily use common code before we can migrate to sensible dependency injection. Being a code smell they even help knowing about places which are still not done.
Summary
In tests traits can be a great tool to reuse common code, while we still consider traits a code smell in almost every other case.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: