During the prep for the Test Double talk I gave at Symfony Live, I read through the paper Evolving an Embedded Domain-Specific Language in Java and it really struck a chord with me for two reasons. Firstly, it gave me further insight in to the theory behind my talk and secondly, because it detailed a process I was planning on going through in the not too distant future. The paper essentially details the author's findings as they studied the API for various tools before designing and build the DSL for jMock.
The main thing I took from the paper was the use of interfaces to limit the available choices while building up "train wreck" style set of method calls.
As an example, take a look at this screenshot from PhpStorm using the master branch of mockery:
The shouldHaveReceived
method returns a Mockery\VerificationDirector
(which
extends Mockery\Expectation
for implementers convenience). Calling with
on
the Mockery\VerificationDirector
actually returns the same
Mockery\VerificationDirector
at which point you could call one of the many
other methods, including another call to with
.
Ladies and Gentleman, this does not make sense. It also makes for a very poor
API experience. Of course you're not likely to write
$spy->shouldHaveReceived("foo")->with("bar")->once()->with("bar")
and you
could argue that the implementation should prevent this crazyness, but I'm sure
you get the point.
For the previously mentioned new TestDouble API for mockery, I decided to have a crack at using interfaces to define the syntax for the DSL, in a way that I think is most suitable for creating readable tests. That way would be (for a spy), specifying the method expected to have been called, (optionally) specifying the arguments it should have received and then (optionally) specifying the number of times you expected the call to happen.
interface Spy
{
/**
* @return CallArgumentVerifier
*/
function shouldHaveReceived($method);
}
The spy interface makes it clear that calling the shouldHaveReceived
method
satisfies the first part of our desired syntax and will give you a CallArgumentVerifier
. Now the CallArgumentVerifier
needs to
make available a set of methods to specify the arguments the call received and
also allow the call chain to continue. To do so, all of the with*
methods will
return a CallCountVerifier
. Simple right? But remember, specifying the
arguments is an optional step, so to facilitate this, we have the
CallArgumentVerifier
extend a CallCountVerifier
, allowing the client to
skip straight to the call count verification methods. Cascading the segregated
interfaces in this way let's us force our particular narrative on the users.
interface CallArgumentVerifier extends CallCountVerifier
{
/**
* @param mixed $arg The first expected argument
* @param mixed $arg,... The subsequent expected arguments
*
* @return CallCountVerifier
*/
function with($arg/*, $arg2..., $arg3...*/);
/**
* @param array $args The expected arguments
*
* @return CallCountVerifier
*/
function withArgs(array $args);
/**
* @return CallCountVerifier
*/
function withNoArgs();
/**
* @return CallCountVerifier
*/
function withAnyArgs();
}
The methods available on the CallCountVerifier
should be the last stop in our
train-wreck call chain, so we simply have them return void.
interface CallCountVerifier
{
/**
* @return void
*/
function once();
/**
* @return void
*/
function twice();
/**
* @param int $count
*
* @return void
*/
function times($count);
}
And that's pretty much it! This spy example is quite simple, but it has a nice effect when plugged in to your favourite IDE. The DSL for Mocks will have a deeper and wider API, so might be more complicated.
As previously mentioned, I've been pairing on this with @karptonite, so would like to thank him for his help. I've no idea how this will pan out once we start trying to build the implementation, but I'm pleased with the progress so far.