fbpx

Method order assertion in PHPSpec: Deferred Expectations

Disclaimer

This post title can be misleading. Method order is just an implementation detail that should not be tested as PHPSpec philosophy is to test your public API, not the implementation as it would lead to fragile tests. Fundamentalists might say that following example should not be performed in PHPSpec but with a functional test. Sadly, when you start with BDD, you may not know that your class will end in a situation where you’re somehow forced to be sure of method invocation order; in such cases you have two choices: drop the spec for this particular class and go on with a functional test or follow this post that provide a solution for the situation where being dependent from implementation could be, somehow, acceptable as test failure due to implementation changes (ie.: method order change) is what we’re trying to reach.

For more information about PHPSpec philosophy, you can read this blog post by Everzet

Scenario

We need a class that keeps a log of operations performed onto an entity in a way that, for each operation, we can recreate a step-by-step status change. For the sake of this example, we log the whole entity in the operation stack though we could keep only property changes.

This is the entity

We introduced a copy method to have full control on the stub: using PHP built in __clone method would make us lose control over the process.

This is the spec file that we wrote before writing the code

 

 

There are two ways to create stubs: one is PHPSpec automagic injection thanks to type hint. Other is to create through Prophecy.

Usually we adopt a strategy where all collaborators are type hinted whereas other doubles are created directly trough Prophecy. This means that you need to check predictions for mocks directly with

$this->prophet->checkPredictions()

If you miss this line, no predictions of mock created thought Prophecy are checked and, probably, your tests will be green even if they shouldn’t.

Writing test before the code helps you to spot this problem as first run should always be a failing one.

“Never trust a test you’ve never seen failing

– Anonymous”

And this is the code

Running PHPSpec will result in green examples

 

 

 

 

By the way this test is not proving anything about test correctness: it says that in output we should have a log with entity and its cloned copies along with operations performed onto original entity. We have no way to check if clonedEntity1 and clonedEntity2 has a copy of the entity before the operations were performed (that is the goal of this spec). We can’t simply check, by getters on clones, if values before operations are kept as them are doubles so they are not actual entities.

Let’s pretend that I shift copy method after original entity modification for error

Running tests again results in

 

 

 

 

This spec is clearly wrong but there is no straightforward solution provided by PHPSpec to get what we need (as said before, PHPSpec is not design for this kind of implementation checks)

Deferred Expectations

When a method is invoked on a mock, PHPSpec/Prophecy immediately checks if it is registered as expected calls; if it is not, an exception is raised. This is the key concept: register expectations (changeProp1 and changeProp2) only if a method is called (copy). In this way if no copy is called before changeProp1 or changeProp2, the expectations are never registered and any call on those method will result in Exceptions.  This is what I call “deferred expectation” (or conditional expectation if you prefer).

This is how Spec file change

If we now run last EntityTransformer code (the one with copy in the wrong place) we get

 

 

 

 

 

Bingo!

Looking at spec code above you will notice that

are registered as expectations only when copy method is called (thanks to closure; this is a deferred behavior). It’s worth to notice that copy will also return first cloned object along with second invocation copy canned method.

If no copy is ever invoked before changeProp* method, example will be broken as no canned response is registered to answer to changeProp* invocation.

If EntityTransformer code is brought back to original and correct version, example will return quickly green

 

 

 

 

If this example does not make any sense to you, just think about Doctrine entity manager: you can expect that persist($entity) and flush should be called but, as long as you don’t guarantee the correct order of this calls, you can never be sure that spec is fine.

That’s all. Hope this quick trick will help you with your spec and, as usual, don’t take this as a rule of thumb: try to keep your spec files independent of implementation as much as you can.

DonCallisto
DonCallisto
Articoli: 21

Lascia una risposta

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.