1

To bring a specific example, I have PromoCode Aggregate Root that consists of PromoCodeUsage Entity which is only controlled by that AR, so that some methods on AR are simply delegated to that Entity, like:

public function useFor(Order $order): void
{
    $this->promoCodeUsage->useFor($order);
}

And some of them are partly delegated, like:

public function applyFor(Order $order): void
{
    if (!$this->published) {
        throw new NotPublishedPromoCodeCanNotBeApplied();
    }

    $this->promoCodeUsage->applyFor($order);
}

My test suite fully covers all PromoCode behavior including PromoCodeUsage functionality, because at that iteration there was no PromoCodeUsage and all logic was mixed in PromoCode. Then I refactored some of that logic into PromoCodeUsage. This test suite for PromoCode had many tests and I was happy I can split it too (but it worked well even after splitting entities). So I created another test suite (PromoCodeUsageTest), where I moved part of tests from PromoCode.

But PromoCodeUsageTests are testing PromoCodeUsage entity through the behavior of PromoCode, same way like it was in original test before splitting. They are not touching PromoCodeUsage directly. Now I have PromoCodeTest suite with: enter image description here And PromoCodeUsageTest suite with: enter image description here

But it is somehow weird, that 1) in PromoCodeTest I omit some tests (that are elsewhere) and 2) in PromoCodeUsageTest I am actually not touching PromoCodeUsage entity. 3) I use Roy Osherove’s template for test naming, and I do not know what method name should I use in test name - from PromoCode or PromoCodeUsage? In my case they are same but they could differ and that idea smells.

If I rewrite PromoCodeUsageTests to test directly PromoCodeUsage entity, I end up with some uncovered methods on PromoCode (that are just delegated to PromoCodeUsage). So that takes me back to my approach to test PromoCodeUsage through PromoCode AR.

Uncle Bob (and others) says it is good practice to test behavior, not API. Does my approach head to conform to that?

Because I feel some smell in my approach, do you? How to do it better?

Tom
  • 189
  • 1
  • 12
  • 1
    http://stackoverflow.com/a/153565/54734 – VoiceOfUnreason May 19 '17 at 04:43
  • 1
    *"in PromoCodeUsageTest I am actually not touching PromoCodeUsage entity"* - not even in Asserts? – guillaume31 May 22 '17 at 09:24
  • @guillaume31 Nope, I am asserting through AggregateRoot. And also, I do not assert too much, I rather have no assertion to know, that action can be performed without throwing exception/error when I already tested all cases that throws expected exception. Because I found out, I would need to have getters on my entities only to be able to perform those assert and I think it is bad, not just because having methods for tests only, but because then I would test API rather than behavior. – Tom May 22 '17 at 09:31

2 Answers2

4

You're correct to think about testing behavior. I assume all the behavior of your aggregate is exposed through the aggregate root, so it makes sense to test through the root. I would just suggest you name your tests to describe the behavior they are testing . Don't use method names in the test names because these could change - this is tying your test names to the internal implementation of your production code.

If a test class is getting very large, it makes sense to break it into smaller classes - there's no rule that you must have 1:1 relationship between test and production classes. However, this may suggest your class, and your aggregate in this case, may have too many responsibilities and might be broken into smaller pieces.

Mike Stockdale
  • 5,256
  • 3
  • 29
  • 33
  • Unfortunately, my AR really needs all this, because it needs to atomically check invariants. It is little complicated because those invariants can be dynamically set with Restriction objects. But thanks for tips, definitely helpful for my confidence and inspiration :) – Tom May 22 '17 at 09:42
1

I tend to see Aggregates as state machines and test them accordingly.

What's important is not in which test file the tests are, but that you test all possible resulting states of the PromoCode aggregate depending on the starting state and the kind of promo code usage/application you're doing.

Of course, this might require looking deep down in the guts of the aggregate, in dependent entities. If you're more comfortable putting in a different test class all the tests whose Asserts look at PromoCodeUsage for instance, then fine, as long as test names reflect the domain and not some technical details.

guillaume31
  • 13,738
  • 1
  • 32
  • 51
  • I decided I will test all through PromoCode AR behavior, but I will separate tests into many Test classes named by specific classes (entities) PromoCode is made of. But I am now stuck with how to name tests of each specific IRestriction in PromoCodeUsage (which server as dynamic business invariant). So I now came with something like `XxxOrderSpecificationRestrictionTest` containing for example `testThroughPromoCodeApplyTo_OrderWithUnfitSpecification_SUTWithEmptyOptions_ShouldNotApply`. But that's weird. Also, what really SUT is, XxxOrderSpecificationRestriction, or AR I am testing it through? – Tom May 22 '17 at 13:55
  • 1
    I didn't quite get what `Restrictions` are about, but I would say that the AR remains the SUT. It either transitions to a new state as a whole or doesn't transition. – guillaume31 May 22 '17 at 14:23