Reputation: 10210
Given the following Value
object (no publicly accessible setters
):
class Address
{
public function __construct(string $addressLine1, string $addressLine2 = null, string $town, string $county, PostcodeInterface $postcode)
{
if (!$this->validateAddressLine($addressLine1)) {
throw new \InvalidArgumentException(...);
}
$this->addressLine1 = $this->normaliseAddressLine($addressLine1);
...
}
private function validateAddressLine(string $addressLine)
{
...
}
private function normaliseAddressLine(string $addressLine)
{
...
}
}
I have the following test class:
class AddressTest extends PHPUnit\Framework\TestCase
{
public function invalidConstructorArgs()
{
return [
['1/1 Greenbank Road', '%$', 'city', 'county', new Postcode('123FX')]
];
}
/**
* @test
* @dataProvider invalidConstructorArgs
*/
public function constructor_with_invalid_args_throws_exception($addressLine1, $addressLine2, $town, $county, $postcode)
{
$this->expectedException(\InvalidArgumentException::class);
$address = new Address($addressLine1, $addressLine2, $town, $county, $postcode);
}
}
As you can see I am currently using a DataProvider
to supply my unit test with data. Doing so results in a large array of values to be tested which are all written manually. Each argument is validated by an appropriate private method. Currently to test those methods I am writing a data provider which contains valid and invalid argument values to test these methods (as below):
// test validation on first argument
["invalid", "valid", "valid", "valid", "valid"],
...
// test validation on second argument
["valid", "invalid", "valid", "valid", "valid"],
...
// test validation on third argument
["valid", "valid", "invalid", "valid", "valid"],
...
// etc.
Is there something in PHPUnit I should be making use of that I have overlooked for this type of scenario?
Upvotes: 0
Views: 1543
Reputation: 17417
I agree with your comment that having validation logic in your value object is a matter of taste, but one major drawback is that it does make unit-testing harder, as you're seeing. If your value object is now responsible for two things (data storage and validation), then testing gets more complicated, especially if you've made the validation logic private.
One approach you might want to consider is to use Reflection to test your private methods directly. You'll find a lot of debate about whether this is bad practice in some of the related questions linked to the side of this one, which I won't go over again here. For my own point of view, I think this is one of the few cases where it makes sense.
You can use something like this to run a private method from the unit test directly:
/**
* @test
* @dataProvider invalidAddressLine1Provider
*/
public function invalid_parameter_throws_exception($invalidParameter)
{
$reflector = new \ReflectionClass(Foo::class);
// The call to newInstanceWithoutConstructor here is important, since the
// constructor is what we're looking to avoid
$instance = $reflector->newInstanceWithoutConstructor();
$method = $reflector->getMethod('validateAddressLine1');
$method->setAccessible(true);
$this->expectException(\Exception::class);
$method->invoke($instance, $invalidParameter);
}
You could also combine the invalid parameters for all validation methods into a single dataProvider, and pass the method name in as an argument, to save duplicating the Reflection code.
public function invalidProvider()
{
return [
['validateAddressLine1', 'invalid value for line 1'],
['validateAddressLine2', 'invalid value for line 2'],
// ...
];
}
Upvotes: 2