Refactoring VoteValidator with tests
A few days ago I decided that the t-shirt a day application doesn’t need the complexity of a vote validator with a validation handler and only a method stating if a vote is valid or not returning a boolean and then starting refactoring.
The process of recfactoring was not painfull thanks to the test suite already there but I must admit that I have spoted some flaws in my VoteValidator tests.
A cleaner VoteValidator
A vote is valid if :
- The voter has note already voted for a Tshirt for the given day
- The Tshirt has not been elected yet
- A voting session is opened for that day
and I wanted the code to express this requirements clearly.
The isValid has been refactored to use 3 private methods each one dealing with one rule :
<?php
class VoteValidator
{
public function isValid(Vote $vote)
{
return $this->voterHasNotAlreadyVotedForDay($vote->voterId(), $vote->day())
&& $this->tshirtExistsAndHasNotBeenElectedYet($vote->tshirtId())
&& $this->aVotingSessionIsOpenedForDay($vote->day());
}
}
?>
This is the point I started to think that I probably should be able to switch lines and still have the test going green and tried to do so. Well, tests were failing.
Testing the right thing
I’ll take a simpler example that the one above with only two conditions to describe what I did wrong when I first wrote the tests.
Let’s define a simple class :
<?php
class Tuple
{
public $a;
public $b;
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
}
}
?>
A tuple is valid if :
- Element A exists
- Element B exists
The tuple validator would have been something like this :
<?php
class TupleValidator
{
private $elementARepository;
public function __construct(ElementRepository $elementRepository)
{
$this->elementRepository = $elementRepository;
}
public function isValid(Tuple $tuple)
{
return $this->elementAExists($tuple->a)
&& $this->elementBExists($tuple->b);
}
private function elementAExists($a)
{
return $this->elementRepository->exists($a) !== null;
}
private function elementBExists($b)
{
return $this->elementRepository->exists($b) !== null;
}
}
?>
In the TupleValidatorTest the method asserting that isValid returns false if A doesn’t exist would have been :
<?php
class TupleValidatorTest
{
private $elementRepository;
public function setUp()
{
$this->elementRepository = \Mockery::mock('ElementRepository');
}
/**
* @test
*/
public function should_return_false_if_A_doesnt_exist()
{
$a = 'A';
$b = 'B';
$tuple = new Tuple($a, $b);
$validator = new TupleValidator($elementRepository);
$this->elementRepository->shouldReceive('exists')->with($a)->andReturn(false);
$this->assertFalse($validator->isValid($tuple));
}
}
?>
The test will pass.
Now if I invert the two test in the isValid method :
<?php
// TupleValidator
public function isValid(Tuple $tuple)
{
return $this->elementBExists($tuple->b)
&& $this->elementAExists($tuple->a);
}
?>
the test is failing because the ElementRepository mock wasn’t expecting a call for "exists" method with B as parameter.
The implementation and the test are tightly coupled, which is bad.
The mistake here is that I don’t set up the system properly. If I want to test that isValid returns false when A doesn’t exist I should ensure that B exists because I don’t want a false positive test passing because B is considered unexistant too.
Let me introduce two more methods in the test class in order to make tests easier to read :
<?php
// TupleValidatorTest
public function elementExists($element)
{
$this->elementRepository->shouldReceive('exists')->with($element)->andReturn(true);
}
public function elementDoesntExist($element)
{
$this->elementRepository->shouldReceive('exists')->with($element)->andReturn(false);
}
?>
We can now write the tests with the correct set up :
<?php
// TupleValidatorTest
/**
* @test
*/
public function should_return_false_if_A_doesnt_exist()
{
$a = 'A';
$b = 'B';
$tuple = new Tuple($a, $b);
$validator = new TupleValidator($elementRepository);
$this->elementDoesntExist($a);
$this->elementExists($b);
$this->assertFalse($validator->isValid($tuple));
}
?>
This test will pass whatever the order of the assertions in isValid is.
I think starting with the test for the positive result helps because you have to set up the whole system in order to make it pass and you have a template for all the negative results.
Here are all the correct tests for the TupleValidator :
<?php
// TupleValidatorTest
/**
* @test
*/
public function should_return_true_if_A_and_B_exist()
{
$a = 'A';
$b = 'B';
$tuple = new Tuple($a, $b);
$validator = new TupleValidator($elementRepository);
$this->elementExists($a);
$this->elementExists($b);
$this->assertTrue($validator->isValid($tuple));
}
/**
* @test
*/
public function should_return_false_if_A_doesnt_exist()
{
$a = 'A';
$b = 'B';
$tuple = new Tuple($a, $b);
$validator = new TupleValidator($elementRepository);
$this->elementDoesntExist($a);
$this->elementExists($b);
$this->assertFalse($validator->isValid($tuple));
}
/**
* @test
*/
public function should_return_false_if_B_doesnt_exist()
{
$a = 'A';
$b = 'B';
$tuple = new Tuple($a, $b);
$validator = new TupleValidator($elementRepository);
$this->elementExists($a);
$this->elementDoesntExist($b);
$this->assertFalse($validator->isValid($tuple));
}
?>
When writing a test be sur to set up the system in a state that allows you to test what you think you are testing.
This is a rather long post and even if I noticed something else during the refactoring I’ll stop now and keep this for a futur article.
Hey ! I’m on Twitter too, if you want to chat about testing or something else. Feel free to comment below as well.