Practical PHP Testing Patterns: Custom Assertion
Join the DZone community and get the full member experience.
Join For FreeSometimes we want to only check the traits of objects and values which are interesting for the current test, not to compare them as a whole with a reference one. Moreover, we want our test to be readable, and complex conditional logic in them is only going to worsen the picture. Reusing some of this logic in more than one test would be nice, as then it will change only in one place.
If only I have an assertion that... is the phrase that Meszaros quotes in his book. The solution to need for new assertions is just to write one yourself.
Whenever you introduce a level of abstraction over common assertions provided by the testing framework, like assertEquals(), or assertTrue(), you are creating a Custom Assertion.
A little bit of work on introducing Custom Assertions, and refine them well, goes a long way in debugging failing tests in the future. It's some minutes of work that try to avoid having to introduce echo statements all over again.
Implementation
To create a custom assertion in PHPUnit, just add assert*() methods on your Testcase Class, like assertObjectIsAdjective() or assertObjectHasNoun().
These methods, as the common assert*() ones, take one parameter to check, and from zero to N constraints it must satisfy. For example, a very specific property may result in a method such as assertObjectIsFast($object), while a trait which must be compared to an expected outcome will result in a assertIsAsFastAs($speed, $object).
In Custom Assertions, you can specify meaningful messages: the testing framework couldn't because its methods are used on a very wide range of objects; yours instead may assume a SomeClass object is passed, or that the parameter is an address or a phonenumber.
You can also include part of the actual or expected parameters in the message, or throw an exception before checking if the actual parameter is in a very strange state.
Summing it up, when finding that assert logic is repeated in different, refactor and apply Extract Method to define new assertions. You can also put these methods on a superclass or a Test Helper if you want to reuse them application-wide.
Variations
- In Custom Equality Assertions, the expected and actual object to verify are passed in, but equality is trickier than simply using == and === and is encapsulated in the assertion method itself. This variation is an equals() as Foreign Method, as Fowler and Meszaros said.
- In Object Attribute Equality Assertion, we don't care about all the object but only about some of its attribute(s). So this method only checks these interesting traits, and is less fragile than a full comparison.
- Domain Assertions are called in domain-specific tests. For example, if your application treats chemical formulas, this assertion may be assertIsHydrocarbion().
- Diagnostic Assertions have more specific messages with respect to standard assertion methods. For example, if you're comparing XML or JSON as strings, you may want instead to see what elements are different, and not only that two long strings do not match.
- Verification Methods incorporate even the act phase into the method, not only the assert one.
Examples
the sample code shows you an example custom assertion for each of the variations. Since the value of messages and assertions is in their failure, they are called with explicitly incorrect values.
<?php
class CustomAssertionsTest extends PHPUnit_Framework_TestCase
{
public function testCustomEqualityAssertion()
{
$this->assertPhoneNumbersAreEqual('5551234', '555 12 33');
}
public function assertPhoneNumbersAreEqual($expected, $actual)
{
$expected = $this->normalizePhoneNumber($expected);
$actual = $this->normalizePhoneNumber($actual);
$this->assertEquals($expected, $actual, "The $actual phone number is not equal to the expected $expected.");
}
private function normalizePhoneNumber($number)
{
return str_replace(' ', '', $number);
}
public function testObjectAttributeEqualityAssertion()
{
$fordCar = new Car(10);
$peugeoutCar = new Car(5);
$this->assertCarsHaveTheSameSpeed($peugeoutCar, $fordCar);
}
public function assertCarsHaveTheSameSpeed(Car $firstCar, Car $secondCar)
{
$firstSpeed = $firstCar->getSpeed();
$secondSpeed = $secondCar->getSpeed();
$this->assertEquals($firstSpeed, $secondSpeed, "The speeds of the two cars differ: they are $firstSpeed and $secondSpeed.");
}
public function testDomainAssertion()
{
$car = new Car(60);
$this->assertCarIsBelowTheUrbanSpeedLimit($car);
}
public function assertCarIsBelowTheUrbanSpeedLimit(Car $car)
{
$this->assertTrue($car->getSpeed() <= 50, "Car's speed is too high: ". $car->getSpeed() . ".");
}
public function testDiagnosticAssertion()
{
$expectedJson = '{"engine" : "good", "hood" : "awful", "tires" : "normal"}';
$actualJson = '{"engine" : "good", "hood" : "good", "tires" : "normal"}';
$this->assertJsonEquals($expectedJson, $actualJson);
}
private function assertJsonEquals($expectedJson, $actualJson)
{
$expectedJson = json_decode($expectedJson);
$actualJson = json_decode($actualJson);
var_dump($expectedJson, $actualJson);
// taking advantage of assertEquals(object, object)
$this->assertEquals($expectedJson, $actualJson);
}
public function testVerificationMethod()
{
$car = new Car(10);
$this->assertNewSpeedCreatesANewValueObject($car);
}
private function assertNewSpeedCreatesANewValueObject(Car $car)
{
$oldSpeed = $car->getSpeed();
$newSpeed = $oldSpeed + 10;
$newCar = $car->setSpeed($newSpeed);
$this->assertEquals($oldSpeed, $car->getSpeed());
$this->assertTrue($newCar instanceof Car, 'Car::setSpeed() does not return a new Car object.');
$this->assertEquals($newSpeed, $newCar->getSpeed());
}
}
/**
* A simple Value Object that our tests use.
*/
class Car
{
private $speed;
public function __construct($speed)
{
$this->speed = $speed;
}
public function setSpeed($speed)
{
// commented to make the test fail and show the assertion message
//return new Car($speed);
}
public function getSpeed()
{
return $this->speed;
}
}
Opinions expressed by DZone contributors are their own.
Comments