Writing isolated unit tests
A guide to writing unit tests
Introduction
When it comes to writing good unit tests it is good to realize that a unit test should test the unit and nothing more. In our case a “unit” would be a class. The class we want to test is called the “class under test” or “CUT” for short.
In order to do this
we will need to follow SOLID design principles and use mocking.
SOLID
Just to refresh your memory we will go trough the necessary principles and explain why they are important for writing proper test code.
Single purpose
Another advantage is that the logic in our classes will be relatively simple, which means we are conforming to the KISS principle (Keep It Simple Stupid). This makes the code in the application or module easy to understand and easy to use.
A single purpose class will, as the name implies, in most cases have a simple interface.
For example:
interface StringValidatorInterface
{
/**
* Method that validates that a variable is a string
* @return boolean
*/
public function validateString();
}
The interface states that there will be classes capable of validating that a variable is of primitive type “string” and nothing more.
A possible implementation of this interface could be:
class
NativeStringValidator
implements
StringValidatorInterface
{
/** @var mixed */
/** @var mixed */
private
$value;
/**
*
Constructor
*
@param mixed $value The value to be validated
*/
public
function
__construct($value)
{
$this->value
= $value;
}
/**
*
{@inheritDoc}
*/
public
function
validateString()
{
return
is_string($this->value);
}
}
Interface Segregation
As can be seen in the section above, we have an interface and an implementation (class). The main reason for this is to avoid our business logic from depending on the implementation. Our business logic does not have anything to do with how we validate a string value, it just needs to somehow be able to validate such values.An added advantage is that we can easily swap the implementation for another one. For example an implementation that uses hamcrest:
class
HamcrestStringValidator
implements
StringValidatorInterface
{
/**
@var Hamcrest\Type\IsString */
private
$matcher;
/**
@var mixed */
private
$value;
/**
*
Constructor
*
@param Hamcrest\Type\IsString
*
@param mixed $value
*/
public
function
__construct(IsString
$matcher,
$value)
{
$this->matcher
= $matcher;
$this->value
= $value;
}
/**
*
{@inheritdoc}
*/
public
function
validateString()
{
return
$this->matcher->matches($this->value);
}
}
Dependency Inversion
These dependencies should have their own tests and as such we should be able to replace them with “test doubles” in our tests. We will discuss the use of test doubles later.
For now it is important to know that in order to test our implementations we need to use dependency inversion to be able to test our logic in isolation.
So let’s imagine we have some business logic we need to test (at some point our logic needs to verify that a variable is of primitive type string):
class SomeBusinessLogic implements SomeBusinessLogicInterface
{
/**
* Constructor
*/
public function __construct()
{
}
/**
* Method that executes the business logic
* @throws Exception
*/
public function executeLogic()
{
...
$stringValidator = new NativeStringValidator($value);
if ($stringValidator->validateString() === false) {
throw new Exception('Value is not of primitive type string!');
}
...
}
}
Instantiating the object in this way binds our logic with the NativeStringValidator implementation. So if we want to changed this in the future we would have to modify the logic itself. When we want to test this class, we have no other option but to test the NativeStringValidator as well. Even though this class will probably have its own test.
It would be better to “inject” this dependency into the logic:
class SomeBusinessLogic implements SomeBusinessLogicInterface
{
/** @var Scuti\SampleCode\StringValidatorInterface */
private $stringValidator;
/**
* Constructor
* @param Scuti\SampleCode\StringValidatorInterface
*/
public function __construct(StringValidatorInterface $stringValidator)
{
$this->stringValidator = $stringValidator;
}
/**
* Method that executes the business logic
* @throws Exception
*/
public function executeLogic()
{
...
if ($this->stringValidator->validateString() === false) {
throw new Exception('Value is not of primitive type string!');
}
...
}
}
The advantage of this should be fairly obvious. We have our loose coupling in place (because we now rely on the interface) and we have inverted our dependency. Our constructor now requires an instance of an object implementing the StringValidatorInterface.
However our string validator can only be instantiated if we already have the value that needs to be tested. This could pose a problem because maybe the value will only be available after executing part of the logic.
There are two solutions to that problem. One of them is designing the string validators in such a way that they can be reused (by moving the parameter $value from the constructor to the method defined by the interface). The other is using a "factory" that produces validators and allows you to pass the value to the factory method:
class
NativeStringValidatorFactory
implements
StringValidatorFactoryInterface
{
/**
*
Produces a NativeStringValidator for the specified value
*
@param mixed $value
*
@return NativeStringValidator
*/
public
function
getStringValidator($value)
{
return
new NativeStringValidator($value);
}
}
class SomeBusinessLogic implements SomeBusinessLogicInterface
{
/**
@var Scuti\SampleCode\StringValidatorFactoryInterface */
private
$stringValidatorFactory;
/**
*
Constructor
*
@param Scuti\SampleCode\StringValidatorFactoryInterface
*/
public
function
__construct(
StringValidatorFactoryInterface
$stringValidatorFactory
){
$this->stringValidatorFactory
= $stringValidatorFactory;
}
/**
*
Method that executes the business logic
*
@throws Exception
*/
public
function
executeLogic()
{
...
$stringValidator
=
$this->stringValidatorFatory->getStringValidator($value);
if
($stringValidator->validateString()
=== false)
{
throw
new Exception('Value
is not of primitive type string!');
}
...
}
}
Most modern frameworks offer solutions to make this easier for us. Often they allow us to somehow specify what objects to inject and take care of injecting them for us when we need them to.
Writing the unit tests
Suppose we want to test the NativeStringValidator. After all we are going to use it in our business logic so we need to make sure it will actually do what we expect it to. The unit test for this class could look something like:
class
NativeStringValidatorTest
extends
TestCase
{
const
TEST_STRING
= 'some
string value';
public
function
testCanInstantiate()
{
$validator
= new
NativeStringValidator(self::TEST_STRING);
$this->assertInstanceOf(
'Scuti\SampleCode\StringValidatorInterface',
$validator
);
}
public
function
testWillValidateString()
{
$validator
= new
NativeStringValidator(self::TEST_STRING);
$this->assertTrue($validator->validateString());
}
public
function
testWillInvalidateInteger()
{
$validator
= new
NativeStringValidator(1234);
$this->assertFalse($validator->validateString());
}
}
Test Doubles
A basic test for the HamcrestStringValidator could look very similar to the NativeStringValidatorTest:
class
HamcrestStringValidatorTest
extends
TestCase
{
const
TEST_STRING
= 'some
string value';
private
function
getIsString()
{
return
new IsString();
}
public
function
testCanInstantiate()
{
$validator
= new
HamcrestStringValidator(
$this->getIsString(),
self::TEST_STRING
);
$this->assertInstanceOf(
'Scuti\SampleCode\StringValidatorInterface',
$validator
);
}
public
function
testWillValidateString()
{
$validator
= new
HamcrestStringValidator(
$this->getIsString(),
self::TEST_STRING
);
$this->assertTrue($validator->validateString());
}
public
function
testWillInvalidateInteger()
{
$validator
= new
HamcrestStringValidator($this->getIsString(),
1234);
$this->assertFalse($validator->validateString());
}
}
class
HamcrestStringValidatorTest
extends
TestCase
{
const
TEST_STRING
= 'some
string value';
const
TEST_INTEGER
= 12345;
private
function
getIsString($expectations
= false)
{
$mock
= $this->getMockBuilder('\Hamcrest\Type\IsString')
->getMock();
if
($expectations)
{
$map
= [
[self::TEST_STRING,
true],
[self::TEST_INTEGER,
false]
];
$mock->expects($this->once())
->method('matches')
->will($this->returnValueMap($map));
}
return
$mock;
}
public
function
testCanInstantiate()
{
$validator
= new
HamcrestStringValidator(
$this->getIsString(),
self::TEST_STRING
);
$this->assertInstanceOf(
'Scuti\SampleCode\StringValidatorInterface',
$validator
);
}
public
function
testWillValidateString()
{
$validator
= new
HamcrestStringValidator(
$this->getIsString(true),
self::TEST_STRING
);
$this->assertTrue($validator->validateString());
}
public
function
testWillInvalidateInteger()
{
$validator
= new
HamcrestStringValidator(
$this->getIsString(true),
self::TEST_INTEGER
);
$this->assertFalse($validator->validateString());
}
}
This way we can test that our implementation does the things we are expecting it to do.
Now since we know the behavior of the class we are able to make the mock object behave the same as the original for the given values, which is all we want to test.
0 Comments:
Post a Comment