Traits are great. You can extract common logic into these mini "classes" and use
them inside multiple classes to
share this logic. When you start testing these traits, things can get a bit bloated because you need to use
a trait
on a class. You might find yourself creating a helper class only for the purpose of testing; a very common case.
Here are 5 alternative techniques that might help reduce some of this bloatiness.
1. use
the trait inside the test class
You have to use
a trait inside a class. That's just how traits work. But instead of creating a class just for testing
purposes, why not use the class we already have? The test class itself!
Here is an example of what that might look like.
We start off with a simple trait that has a single method that returns a provided boolean
state. Yes that is a dumb
method; but it's only to get the point across.
<?php namespace App\Mixin; trait MyTrait { public function returnState(bool $state): bool { return $state; }}
Now that we have the trait; we can use
it inside the test class and perform some assertions.
use App\Mixin\MyTrait;use PHPUnit\Framework\TestCase; class MyTraitTest extends TestCase { use MyTrait; public function testReturnState(): void { self::assertTrue($this->returnState(true)); self::assertFalse($this->returnState(false)); }}
That was easy. No overhead of a custom class and fully tested! However, there might be two potential problems with this approach:
- The method name could be the same as one provided by the test class itself (naming collision).
- There is no isolation; so the test class could actually influence the test results. For example when using a
static
variable inside the trait; that would be applied on the test class and can influence other test cases.
These drawbacks lead us to our second technique.
2. Scoping in an anonymous class
PHP 7 introduced us to anonymous classes. These classes can also extend other classes, implement interfaces and use traits. While this technique still introduces a custom class, we do so "on the fly", and it is fully scoped to the test case only; making it completely isolated from outside interference.
Building on top of our existing example; here is what that would look like.
use App\Mixin\MyTrait;use PHPUnit\Framework\TestCase; class MyTraitTest extends TestCase { public function testReturnStateAnonymous(): void { $trait = new class { use MyTrait; }; self::assertTrue($trait->returnState(true)); self::assertFalse($trait->returnState(false)); }}
If only there was a way to reduce the anonymous class to a single line... a helper method perhaps?
3. Using a getObjectForTrait
"mock"
As it turns out, PHPUnit has a method called getObjectForTrait
that creates and returns an object that uses the
provided trait. This method can also receive arguments needed to create a class, in case your trait implements a
__construct()
method that needs these arguments. This cleans up our test case a bit.
use App\Mixin\MyTrait;use PHPUnit\Framework\TestCase; class MyTraitTest extends TestCase { public function testReturnStateObject(): void { $trait = $this->getObjectForTrait(MyTrait::class); self::assertTrue($trait->returnState(true)); self::assertFalse($trait->returnState(false)); }}
It doesn't get much simpler than this; but our use case is also very simple. What if it was more complex, with calls to other methods that are not on the trait, or methods that should only work when a class implements a certain interface?
4. Using a MockForTrait
mock object
Let's update our trait to include a method that references a method not on the trait.
trait MyTrait { // ... public function callUnavailable(bool $state): bool { return $this->unavailableMethod($state); }}
Looking back at the previous techniques, we could:
Use
it on the test class and implementunavailableMethod
there too- Create an anonymous class that implements the
unavailableMethod
However, what if we would rather mock that method to be able to assert the times the method was called, and with what parameters?
We can use the getMockForTrait
method provided by PHPUnit. This helper method creates a mock object that uses the
provided trait. We can provide an array of methods we would like to be mocked. This gives us way more testing
possibilities.
use PHPUnit\Framework\TestCase; class MyTraitTest extends TestCase { public function testCallUnavailable(): void { $trait = $this->getMockForTrait(MyTrait::class, [], '', true, true, true, [ 'unavailableMethod', ]); // Here we can assert the times the method was called, and with what parameters. $trait ->expects(self::exactly(2)) ->method('unavailableMethod') ->withConsecutive([true], [false]) ->willReturnArgument(0); // still return the provided state self::assertTrue($trait->callUnavailable(true)); self::assertFalse($trait->callUnavailable(false)); }}
Now there is still the use case of implementing an interface
.
5. Using an abstract
helper class
Sometimes you'll have a trait that is only supposed to be used for classes that implement a certain interface
. So lets
say we have a complex interface called App\Contract\ComplexInterface
which has a bunch of functions that have no part
in the trait. Looking at our earlier techniques we can only use a (anonymous) helper class that implements that
interface, and uses the trait. This is quite annoying as we now need to at least implement al these methods. Not ideal.
Fun fact: Did you know you can implement an interface on a class without actually adding any of the methods? PHP
doesn't mind at all, if you mark your class as abstract
. PHP figures you will implement these methods on the concrete
class that extends from this class. It actually marks these methods as abstract
themselves! This is great news for us
as we can now use the getMockForAbstractClass()
method from the PHPUnit test class.
The getMockForAbstractClass()
creates a mock object, but only mocks the abstract methods. It keeps concrete methods
intact. So in our use case every method from the interface will be mocked, but the methods from the trait will not!
We will implement an isComplex()
method on the trait that checks if the class is an instance of ComplexInterface
.
use App\Contract\ComplexInterface; trait MyTrait{ public function isComplex(): bool { return $this instanceof ComplexInterface; }}
Now we can create an abstract
helper class that implement the interface and uses our trait.
use App\Contract\ComplexInterface; abstract class HelperClass implements ComplexInterface { use MyTrait;}
At this point we can use getMockForAbstractClass()
to create a mock object based on that class and perform our
assertions. And to prove our use case actually works, we also use technique 3 to show that will return false
.
class MyTraitTest extends TestCase { public function testIsComplex(): void { $trait_mock = $this->getMockForAbstractClass(HelperClass::class); $trait_object = $this->getObjectForTrait(MyTrait::class); self::assertTrue($trait_mock->isComplex()); self::assertFalse($trait_object->isComplex()); }}
Bonus: easily test protected
methods on a trait
Testing protected
methods usually implies you either test them through another method that calls these methods, or you
extend the class and update their visibility to public
by overwriting them and calling the parent. Traits, however,
have a special trick up their sleeve: you can update their visibility while use
-ing them. Here's how that works:
trait MyTrait { protected function myProtectedMethod(): bool { return true; }} class MyTraitTest extends TestCase { public function testMyProtectedMethod(): void { $trait = new class { use MyTrait { myProtectedMethod as public; // make the method public } }; // We can now simply call the method, as it is now public in this test class. self::assertTrue($trait->myProtectedMethod()); }}
So in conclusion; traits are still great, and testing them is actually quite easy; even in complex situations. Did you learn any new techniques or think someone else needs to learn them? Then please share this post.