Testing Traits in PHPUnit cover image
Image by Mae Mu

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:

  1. The method name could be the same as one provided by the test class itself (naming collision).
  2. 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:

  1. Use it on the test class and implement unavailableMethod there too
  2. 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());
    }
}

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.

Share this article

Twitter Facebook LinkedIn