Start The World's Best Introduction to TDD free...
while you still can!

Simple Design, Evolutionary Design, Microtechniques, Refactoring Comments

Today I read Jason Gorman’s “TDD Under the Microscope: One Outcome Per Test”. I like this and I propose one step more: the emergence of two interesting outcomes itself signals a potentially-helpful abstraction.

First, let me restate the principle in Jason’s article, which I tend to follow:

When one action leads to two interesting outcomes, consider writing two tests with the same action.

… but it gets even better when I go further.

When one action leads to two interesting outcomes, I consider those two outcomes as two listeners to the some (new, emerging) domain event, particularly when those two interesting outcomes seem independent of each other.

Jason’s Example

To return to Jason’s example, the action is “buy item X with payment card Y”. The two interesting outcomes, which he writes as assertions, are “check the inventory of X” and “verify the attempt to charge payment card Y”. It seems to me that these are independent outcomes… although perhaps not. (I’ll come back to this later in this article.) I try to model them as either:

  • two implementations of the same interface, or
  • two listeners to the same domain event

Finding the Abstraction or Domain Event

What’s the abstraction? What’s the domain event? Well… what is the Good Business Reason to charge payment card Y and change the inventory of item X? It seems that we have sold an item to a customer—or at least have been asked to sell an item to a customer. So the domain event could be “sold item X to customer Y”. And, for now at least, it might be good enough for our purposes to let the payment card stand in for the customer. (That’s the Memento pattern!)

But that might seem weird: “buy item X with payment card Y” seems to result in “Yes, item X was bought with payment card Y”. That doesn’t seem very helpful as a test, because it seems tautological. But the principle is sound, so how could this be helpful? Maybe we can improve the design a slightly different way.

Let’s Do Some Really Fast TDD In Our Heads

Well… what could possibly stop us from allowing item X to be bought with payment card Y?1 I often use the answers to this question to add items to my test list. And in the process, I suddenly realize that what we truly have is closer to this:

  • Charging payment card Y might fail, and if it does, then the purchase must fail. (I assume so. It seems sensible.)
  • Updating the inventory of item X might fail, but if it does, that’s a bookkeeping issue we can fix later, so the purchase does not need to fail. Maybe we log a future task to check the inventory. Maybe this becomes a new feature somewhere….
  • The inventory of item X might be lower than the number needed to fulfil the purchase, and if it is, then the purchase must fail. (Or must it? Keep reading.)

It might make sense to check inventory first, then try to charge the payment card, but it might be better for business to charge the payment card first, then check the inventory. (Someone will have to decide this.)

Even if we don’t extract an abstraction nor introduce a domain event, merely thinking about it helps us identify some complications worth understanding before we commit too much to building the feature.

What this tells me is that there are two different candidate domain events: “purchase attempted” and “purchase approved”. Charging the card and checking the inventory both listen to “purchase attempted”, whereas update the inventory listens to “purchased approved”. And charging the card fires “purchase approved” when it completes successfully. It seems that “assert inventory is reduced to 6” makes more sense in the “purchase approved” tests than in the “purchase attempted” tests.

Jason’s original test seems to me like a test for “attempt purchase”, which (in my new conception of the design) would result in either “purchase approved” or “purchase rejected”. And then from there, if the purchase were approved, then and only then would we need to update the inventory for the purchased item. What seemed like they might be independent listeners to the same domain event end up actually being outcomes of two different actions! This happens quite often to me when I try to identify the domain events in even the simplest-looking workflows!

But wait… I have 25 years of experience practising Evolutionary Design and TDD. Maybe that’s the only reason I can see all this. Maybe you worry you won’t see this. I don’t think that’s true; I think you can see it. At a minimum, I feel confident that you could build the habits to see it.

The Long Way Remains Safe

I desperately want to emphasize now that I would have discovered all this if I’d gone “the long way”. It is enough to start with Jason’s tests and follow the Simple Design Dynamo. I would have noticed duplication in the tests, I would have wondered how to remove it, and then I would probably have experimented with doing it. In particular, I’d have noticed some (very slightly) complicated logic, such as cases for:

  1. inventory enough and card payment succeeds
  2. inventory enough and card payment fails
  3. inventory not enough and card payment succeeds
  4. inventory not enough and card payment fails

This would have led me to ask the question, "Wait, if card payment fails, then who cares about the inventory? And if the inventory isn’t enough, then who cares about the card payment? Oh! But wait, wait! Wouldn’t it be better for business to charge the card anyway, then figure out later whether we can source the product even when inventory is low?! I mean, the inventory system isn’t perfect, and we’d hate to lose a sale because the inventory system incorrectly thinks we don’t have enough stock on hand to fulfil the purchase…

“Aha! There’s no point in updating the inventory until we know the payment succeeds, so there’s some kind of intermediate result there. It would be easier to test ‘update inventory’ by letting that be the outcome from a successful purchase and not merely an attempted purchase.”

The key point here is that even if I didn’t quickly see all this in my head, I would have seen it when I started trying to make tests pass, then I would have been able to refactor in that direction. The more I practised, the easier this felt and the quicker I went, until eventually I began to see the shortcuts sooner by doing TDD in my head.

A Conclusion

Here are some related principles I use in software design:

One Action Per Test

Insist on only one action per test. If you discover the structure “action, then assertions, then action, then assertions”, then almost always split this into separate tests for each action.

If you do this, then you’ll probably discover that it helps to identify the state of the system just before the second action, rather than needing to execute the first action to reach that state.

One Outcome Per Test

Always be open to experimenting with one outcome per test. If you want to check multiple outcomes in the same test, then assume that the workflow that generates multiple outcomes is “too complicated” and look for interesting intermediate results or emerging domain events.

If you do this, then you’ll probably simplify the tests at the same time as you better understand the tedious details of the workflow, especially error conditions and mutually-exclusive states. If this helps you avoid allowing your system to reach what should be an impossible state, then you’ve made life much easier for future you!

One Assertion Per Test

Try hard to write only one assertion per test. (This is not quite the same as “one outcome per test”.) If you need more than one assertion to describe a single logical outcome, then consider packaging all the outcomes into a single bundle (struct, object, dictionary).

If you do this, then you might discover helpful missing elements in your design. This happens to me especially when I struggle to name some bundle of things that evidently belong together, then finally find a sensible name. I don’t remember the last time I regretted doing this.

Multiple Truly-Independent Outcomes are Listeners to a New Domain Event

If you have more than one truly-independent outcome to a single action in a test (or group of tests), then consider them as independent listeners to a domain event. Maybe even design them using the Publish/Subscribe or Observer/Observable pattern.

Trying to make them truly independent avoids potential defects and simplifies tests, because most programming languages force you to choose the sequence of statements, even when they are truly independent of each other. It can both be simpler and build more confidence to check each outcome in isolate from the other, both because you avoid variations in the combinations of states that don’t matter or should be impossible and because you open up the possibility for making behavior asynchronous with almost complete confidence that doing so will create no additional risk worth checking with a complicated automated test.

At the very least, trying to do this helps you identify the parts of the workflow that truly need to be arranged in a sequence and the parts that can be treated entirely independently and perhaps run in parallel. The resulting design becomes typically simpler to test, requiring fewer tests that run smaller parts of the system in more isolation. It also becomes easier and safer to add parallelization. Simpler, faster, clearer.


  1. This is an example of Chet Hendrickson Technique, which I use as part of Value-Driven Product Development to explore either a feature, a feature area, or even an entire nebulous product idea in detail. I’m told it’s related to “context diagramming”.↩︎

Comments