Logan Bailey

Adventures In Web Development

Blog, About, GitHub, and LinkedIn

Traits are a feature of the php language that allow for horizontal code reuse. In its simplest form it can be described as interpreter level copy and pasting. When sharing code, whether through inheritance or traits, I've always struggled with how to test the code with the least amount of work. Historically, I've tested traits with partial mocks the trait. PHPUnit makes this easy with getMockForTrait().

trait Contact {
    public function getEmailAddress() {
        return 'admin@mycompany.com';
    }
}

class ContactTraitTest extends \PHPUnit_Framework_TestCase {
    public function testGetEmailAddress() {
        $trait = $this->getMockForTrait(Contact::class);
        $this->assertSame('admin@mycompany.com', $trait->getEmailAddress());
    }
}

The tests for objects that used the Contact trait would not test the getEmailAddress function, it was already tested. This worked nicely, code coverage was high and there were no copy and paste tests. However, this has always seemed wrong. Unit testing should be testing the public interface of the unit. This doesn't test the public interface of the unit, it actually provides false positives where the tests are passing but there could be a bug in the code. In the following example the missing @ in the email address would go unnoticed.

class SupportGroup {
    use Contact;

    private $domain = 'mycompany.com';
    
    public function getEmailAddress() {
        return "support{$this->domain}";
    }
}

This is a contrived example, but illustrates the potential introduction of unnoticed bugs, while the tests are passing. A better solution is to create a trait, e.g. TestContractTrait, in the tests directory. This trait would include tests for the public api of the Contact trait. All objects that use the Contact trait would use the TestContractTrait trait. Class specific functionality would be tested in the object's test case. This works nicely as it mimics the same format as the unit under test's implementation.

trait Contact {
    public function getEmailAddress() {
        return 'admin@mycompany.com';
    }
}

class AdminGroup {
    use Contact;
}

class SupportGroup {
    use Contact;

    private $domain = 'mycompany.com';

    public function getEmailAddress() {
        return "support@{$this->domain}";
    }
}

trait TestContractTrait {
    abstract public function getTestObject();

    public function testGetEmailAddress() {
        $trait = $this->getMockForTrait(Contact::class);
        $this->assertSame('admin@mycompany.com', $trait->getEmailAddress());
    }
}

class AdminGroupTest extends \PHPUnit_Framework_TestCase {
    use TestContractTrait;

    public function getTestObject() {
        return new AdminGroup();
    }
}

class SupportGroupTest extends \PHPUnit_Framework_TestCase {
    use TestContractTrait;

    public function getTestObject() {
        return new SupportGroup();
    }

    public function testSupportGetEmail() {
        $group = new SupportGroup;
        $this->assertSame('support@mycompany.com', $group->getEmailAddress());
    }
}

The code reuse in the example is minimal and new functionality added to Contract and tested in TestContractTrait will automatically be tested for each class that uses that trait. The API is completely tested.

Posted In:
php phpunit testing