A common concern that gets raised about using test doubles (mocks, stubs, spies etc) , is that of the configuration of the test double stubs or expectations being out of sync with the signatures of the actual type. Probably best explained with an example.
interface UserRepository { }
class Foo
{
/* ... */
function bar()
{
$this->userRepository->delete(123);
}
}
/** @test */
function should_delete_user_123()
{
$userRepository = Mockery::mock(UserRepository::class);
$userRepository->shouldReceive("delete")->with(123)->once();
$foo = new Foo($userRepository);
$foo->bar();
}
Despite the delete
method not existing on the UserRepository
interface, this
test will pass. For Mockery, this is by
design. When I'm in my TDD loop, , I'm designing the UserRepository
interface
as I develop the Foo
service, programming by wishful thinking. In order for
Foo
to do bar
, I'd like to assume I have a UserRepository::delete
method, but I'll care about adding that later. The problem manifests when we
don't necessarily remember to deal with it later. We should notice the problem
when we run some higher level test, but they don't always exist and even if they
do, we might make the mistake of adding the delete
method to the concrete
UserRepository
used in that higher level test, rather than the abstract. All
the tests will pass, but things still won't be quite right.
The ruby community came up with a solution to this, rspec-fire, which was subsequently made obselete in favour of verifying-doubles in rspec core. This works by allowing you to program by wishful thinking, until you actually create the class, at which point rspec will check to make sure the method you are stubbing or expecting actually exists. Rspec will also check the arity of the stubs and expectations against the real thing.
This sounds great, but kind of annoys me. Just because a class or type exists, doesn't mean it's API is finalised. I would prefer to continue programming by wishful thinking within my TDD loop.
So how are we going to deal with this problem in PHP? For methods that don't exist at all, Prophecy will throw exceptions if you try to set up stubs or expectations.
There was 1 error:
1) ProphecyTest::prophecy_test
Prophecy\Exception\Doubler\MethodNotFoundException: Method `Double\UserRepository\P1::update()` is not defined
I don't believe PHPUnit mocks has any such feature at present, but it looks like something is in the works.
Mockery comes with a global configuration option to prevent stubbing and expecting methods that don't exist yet, that is disabled by default. You can turn it on in your test bootstrap. I like to turn it on for test runs outside the TDD loop. These test runs are more looking for regressions like on a CI server, rather than helping me develop behaviour in the system, so it seems sensible to verify we aren't doing anything stupid. I get to ignore warnings during my TDD loop, but get the safety net of having them verified at a later date.
Mockery::getConfiguration()->allowMockingNonExistentMethods(false);
As for the arity of existing methods, I think that's a problem best solved with the proper use of PHP's type hints.