Testing With Traits Posted on April 5th, 2018
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.