It’s an extremely hot day in London, and I’m staying indoors until the temperature falls below 30 degrees C. Too hot for chores or putting up more of my flatpack backlog, I thought it might be fun to do a “play-through” blog post about one of the practical exercises from the Codemanship TDD workshop.
The exercises comes just after we discuss basic software design principles on the first day of the course. At this point, we’ve covered Red-Green-refactor, and dwelled on the refactoring discipline in previous exercises. Now we’re scaling things up a little from FizzBuzz-scale problems to a more business-like set of requirements.
From the book (page 101):
Test-drive some code that manages the stock and orders of a CD
warehouse. Customers can buy CDs, searching on the title and the
artist. Record labels send batches of CDs to the warehouse. Keep a
stock count of how many copies of each title are in the warehouse.
Customers can only order titles that are in stock. Use dependency
injection to fake credit card payment processing, so we can get on
with our CD warehouse design without worrying about how that
will be done.
Customers can leave reviews for CDs they’ve bought through the
warehouse, which gives each title an integer rating from 1- 10 and
the text of their review if they want to say more.
I almost always start by listing use cases, and enumerating key scenarios for each one. (This is essentially the Test List pattern from Kent Beck’s Test-Driven Development by Example book.) Buried in this text is a handful of use cases we need to satisfy.
- Buy a CD
- Payment accepted
- Payment rejected
- Out of stock
- Search for a CD (by title and artist)
- Warehouse has matching title
- No match found
- Receive batch of CDs
- Of existing title in catalogue
- Of new title (add to catalogue)
- Of multiple titles
- Review a CD
- Just a rating
- Rating and review
In this exercise, we’re going to work are way through this list of test cases, doing the simplest thing possible to pass each test, and flesh out a design that not only passes all of these tests, but also satisfies some basic design principles:
- The code must be easy to understand
- The code must be low in duplication (in this exercise, we’ll apply the Rule of Three – if we see three examples of duplicated code, we’ll refactor it to make it D.R.Y.)
- The code must be simple (long methods, deep-nested if’s and loops, etc, will be refactored to make them simpler)
- The modules should do one job only
- The modules should Tell, Don’t Ask (I instruct students to look for a code smell called Feature Envy)
- Module dependencies should be easily swappable by dependency injection
The exercise is a TDD exercise, so we’ll start by writing a failing test, quickly get that test passing in the simplest way we can think of, and then – and this is the point of this exercise – stop and do a little code review.
Yes, you read that right. Not on a pull request. Not at the end of a sprint. A mini code review on every green light.
We’ll pause, go through all the code we’ve added or changed, and apply our checklist. Any code that doesn’t tick all the boxes should be refactored until it does.
Let’s start with “Buy a CD – payment accepted”. First, I’ll write an empty Mocha test for that scenario.
I’ve interpreted the requirement to mean that when you successfully buy a copy of a CD, the outcome is that the stock count of that CD is reduced by one. (Many pairs interpret managing stock count as a use case, but it really isn’t. A use case is triggered by a user action, like buying a CD. The stock changes as an outcome.)
Let’s fill in the blanks, starting with the assertion:
And then let’s work backwards to the set-up via the action:
Remember that the payment being accepted is part of the set-up for this scenario, so – as per the instructions – we do need to provide that information somehow. Since, in real operation, that data will come from an external source (the payments processor), we use a stub to provide it in the test.
This is our first failing test. Passing it is easy.
This brings us to our first green light, which means it’s time to do a mini code review.
- Is my code easy to understand so far? Well, I think so. But what do you think?
- Is there any duplication we need to worry about yet? Not that I can see.
- Is the code simple? It’s early days, but so far, yes.
- Does each part do one job? See above. For this single test, there’s only one job to do.
- Can I see any Feature Envy? Nope.
- Are the dependencies swappable? There’s only one so far – in the class CD in the payments object – and that’s being passed in from outside. So, tick.
- Does CD depend on the implementation of StubPayments? Nope. But, right now, I have got all my code in a single file with the test code. That needs fixing, so I’ll split them into their own files, and move the source code into the root folder away from the tests.
Let’s move on to the next test on our list, “Buy a CD – payment rejected”.
This is our next red light. To pass this, we now need to have the CD ask the payment processor to give us a response, as defined by the stub in our test.
The stub returns whatever response the test tells it to. Stubs should never get any smarter than that.
So, we’re on our second green light. Time for a mini code review. One thing that jumps out at me is this line here in CD’s buy() method:
payments.process doesn’t read as a question. We could make the IF statement clearer by introduced a variable:
The rest of the code ticks the other boxes, but we can see there’s some duplication emerging in our test code.
In this exercise, we’re applying the Rule of Three – if we see three examples of duplication, we refactor. So let’s hang fire on this for now.
Time for the next test.
Passing this test is easy.
Another green light means it’s time for another mini code review. The buy() method of CD has two branches, which is right on the edge of my personal tolerance for complexity. I think I can live it with it, but another IF statement and I’d take action.
Now, about that duplicated test set-up code: we have three examples, which means it’s time to do something about it.
First, let’s tackle the most obvious duplication.
We must take great care when removing duplicated set-up code from tests that we don’t end up with a situation where developers have to keep referring to code outside of each test to understand the scenario. In this case, the credit card details aren’t important. (If we added test cases for bogus cards etc, then it would be different.) So I think we can safely factor our the credit card declaration without impacting readability.
With the rest, it’s not so straightforward. We need to know what the initial stock of the CD is. We need to know what response the payments processor will give. If we wrote more and more tests with this set-up, I would argue for factoring out shared set-up code to limit how much of our test code is bound to the implementation. But, as this is the third and final of these tests, I don’t think it’s worth it. So I’m living the test code as it is and moving on to the next use case.
Coming in Part #2 – Searching for CDs…