ciaranmcnulty / driving-design-through-examples

22 stars 3 forks source link

TDD / BDD newbie question regarding the use of stubs... #2

Open hub20xx opened 8 years ago

hub20xx commented 8 years ago

Hello,

I'm learning TDD / BDD / DDD and I was using your presentation (which I had the opportunity to see live by the way) as a tutorial (in the sense of going through each slide and typing and doing it on my machine).

I was wondering and would like to ask you why you don't use stubs in certain specs.

For example, for the origin and destination airports in the RouteSpec and why not defining what they will return like this:

function it_has_a_string_representation(Airport $origin, Airport $destination)
{
    $origin->asCode()->willReturn('LHR');
    $destination->asCode()->willReturn('MAN');
    $this->beConstructedFromTo($origin, $destination);
    $this->asString()->shouldReturn('LHR to MAN');
}

instead of:

function it_has_a_string_representation()
{
    $this->beConstructedFromTo(Airport::fromCode('LHR'), Airport::fromCode('MAN'));
    $this->asString()->shouldReturn('LHR to MAN');
}

I'm green at TDD / BDD and I read and heard that it's best to use doubles for dependencies, that's why I was surprised to see that you instantiate the Airport value objects here.

Is it because they're value objects that you decided to use those dependencies in the Route spec?

I also noticed something similar (or at least it looks like so to me) but for a more complex case in the TicketIssuerSpec:

function it_issues_a_ticket_with_the_correct_fare(\FareList $fareList)
{
    $route = Route::fromTo(Airport::fromCode('LHR'), Airport::fromCode('MAN'));
    $flight = new Flight(FlightNumber::fromString('XX001'), $route);
    $fareList->findFareFor($route)->willReturn(Fare::fromString('50'));
    $this->beConstructedWith($fareList);
    $this->issueOn($flight)->shouldBeLike(Ticket::costing(Fare::fromString('50')));
}

Doesn't this make the tests / specs more fragile?

Thank you very much for your presentation showing the process step by step and for putting the code too, I've learned a lot from it!

Take good care,

Hubert

PS: I didn't know where to ask about the code so I created an issue (even if it is not really an issue, except maybe for me having an issue with understanding those things... ;) ). Hope it's OK for you.

ciaranmcnulty commented 8 years ago

Hi, thanks for the questions!

Like everything else in life, this is a trade-off so I'm not going to pretend there's a right answer.

Using real objects rather than doubles:

  1. Makes the test more concise (your example is a good one, in my opinion the second case is far more readable).
  2. Exposes us to danger that the test is less isolated and failures may be higher to spot

In general I find using concrete Value Objects in tests makes sense (we don't double other values like integers or strings, after all). It tends to lead to more readable tests and when a VO is broken it's been, in my experience, easy to see what the problem is even though many tests fail as a result.

I wouldn't restrict the approach to value objects either; a lot of the time when I am doubling a concrete class I stop and think about whether it would make more sense to either:

More and more I'm finding that one of these solutions seems better; either the class is connected to infrastructure or some other bounded context that should be isolated anyway, or is trivial enough to use directly.

This is something I'm constantly trying to evaluate as I do it, and my advice may well be different in a couple of years of doing it, but that's where I am right now.

The main thing is to try to make the tests very clear for others, without making it too hard to debug failures.

Hope this helped?

mablae commented 8 years ago

As @ciaranmcnulty asked me to post this: (https://twitter.com/CiaranMcNulty/status/726343910201692160)

Another point for having concrete classes of Value Objects 
instead of Doubles would be the fact that the VOs SHOULD be always side-effect free.

Mocking or Doubles are most often needed to strip out the external dependencies. 
Value Objects do not have these, by the definition. "
ciaranmcnulty commented 8 years ago

Yeah that's a good point I didn't mention.

There are many reasons for using Doubles:

  1. Test isolation / localised failures - one broken component leads to one broken test
  2. Isolation from side-effects - you don't want to accidentally trigger activity in other systems.
  3. Lack of availability - some classes depend on a PHP extension or some real infrastructure to even instantiate
  4. Lack of convenience - maybe it's hard to get the real objects to behave the way you need for your test?
  5. Performance - the real class may use a lot of memory or CPU

None of these really apply to Value Objects except 1, which is why I focussed there, but it's worth mentioning the other reasons.

MarkRedeman commented 8 years ago
  1. Test isolation / localised failures - one broken component leads to one broken test

Does using doubles instead of concrete instances of a value object really lead to a better test isolation though? I'm asking because in my (minor) experience most of my value objects tend to brake when I change their interfaces. This means that if my tests were to use doubles I would have to change those tests because the double's interface has changed.

For me one of the disadvantages of not doubling a value object is that sometimes I have value objects which depend on other VOs and those VOs might also depend on other VOs. It can get quite tedious and annoying to write tests for such use cases since you have to build each value object separately. However I think this is more of a design issue than a testing (using doubling vs concrete classes) issue and it could probably be resolved by either rethinking the model or if needed using factory / builder classes.

ciaranmcnulty commented 8 years ago

@MarkRedeman In the simple case that if the VO breaks lots of tests break. That's all really.

I agree about construction of VOs getting tricky but then it will be tricky to construct them in your real code too - it can be solved using the various creational design patterns (I tend to like Builders)

lucascourot commented 8 years ago

Even if you wanted, you couldn't mock Values Objects because they are final classes.

ciaranmcnulty commented 8 years ago

In this case, but there's no requirement for a VO to be final (it's just a good idea)

hub20xx commented 8 years ago

@ciaranmcnulty Thank you very much for the explanation! :)

everzet commented 8 years ago

There are many reasons for using Doubles: ...

In a traditional London school sense, the primary reason for using doubles is to help you design communication between objects by exposing it. If assertions is the way to put design pressure on objects themselves, doubles is the way to put design pressure on their interactions.

My rule of thumb for all doubles is - use them when you care about the object collaboration. I find that I rarely care about collaboration with Value Objects, Collections and other simple value representations.