Back in early 2015, on my old blog, I ran a series of articles in which I implemented a solution to a popular code kata. I followed the strict test-driven development (TDD) cycle, and when it came to the REFACTOR step I tried to drive all of my choices using connascence. After 7 or 8 articles we (me and the regular commenters) ended up quibbling over which kinds of connascence I had, which should be tackled first, etc. It was a useful exercise, because we all learned a lot about connascence and how to deal with it.
So now, like a glutton for punishment, I’m going to do something similar again.
I’m going to tackle Dave Thomas’s Back to the Checkout kata (again). I’m going to show you every step of the way while documenting my thinking as openly and as accurately as I can. I want to explore TDD’s REFACTOR step again, this time from the point of view of the 4 rules of simple design and with a particular focus on making the implicit explicit. And because this will be quite a long series of articles I’ll be chucking them at you twice each week instead of the usual once. (You lucky people.)
And if it goes well — and maybe also if it doesn’t — I’ll probably do it again in a different (kind of) programming language. Because I think an important part of knowing how to refactor is also knowing how language affordances enable or disable the various solution patterns we might consider.
I’m going to follow Dave Thomas’s lead from the original kata statement and implement my tests strictly in the order he presents them.
So without further ado, here’s my first test:
And when I run it:
(I’m using ES6, and Jest, and make. Because those are the tools I’m comfortable with.)
Some might say that a test that doesn’t compile isn’t a “failing test” in the terms of TDD. I beg to differ, and we’ll explore that topic a little more in a moment.
But first I’m going to commit this:
Install toolchain and write first failing test. You can follow along with these commits via the public git repository at https://github.com/kevinrutherford/hc-js-checkout, where you can also look into my
Makefile and other details of my toolchain. (For the most part I expect that stuff to not be relevant to the problem we’re exploring here, so I probably won’t mention it again.) Note that due to holiday and work commitments I’m writing these articles in batches a few weeks ahead of publication time; so in the repo you’ll already be able to see where this is going, if you want to look ahead.
Now, what to do about that failing test…
I’m going to adopt Keith Braithwaite’s TDD as if you meant it style. TDDAIYMI has the following, stricter, rules:
Write exactly one new test, the smallest test you can that seems to point in the direction of a solution
See it fail
Make the test from (1) pass by writing the least implementation code you can in the test method.
Refactor to remove duplication, and otherwise as required to improve the design. Be strict about using these moves:
you want a new method—wait until refactoring time, then… create new (non-test) methods by doing one of these, and in no other way:
preferred: do Extract Method on implementation code created as per (3) to create a new method in the test class, or
if you must: move implementation code as per (3) into an existing implementation method
you want a new class—wait until refactoring time, then… create non-test classes to provide a destination for a Move Method and for no other reason
populate implementation classes with methods by doing Move Method, and no other way
Keith doesn’t recommend that we do this in our day job, but in my context here it feels like exactly the right choice. By following these rules I will be forced to refactor my solution into existence, without the aid of any magical starting design that I would then need to justify.
So I change my failing test as per Keith’s rule 3:
Pass the test in TDDAIYMI style.
Does this feel like “cheating”? It’s thought-provoking though, isn’t it? I have a passing test, and yet I’ve done much less “design” than I might usually have done. And hopefully the point of this will become clear after the next couple of tests and refactoring steps.
Speaking of which… Can you see any refactoring that needs to be done? Actually, now you mention it, I’m looking at the test description in the
make output and thinking that “back to the checkout has zero price before items are scanned” doesn’t make a lot of sense. I go back to Dave Thomas’s Ruby example and realise I haven’t quite transliterated his intention correctly. So I refactor the test to be this:
Now the test run tells a better story:
I wish this output had less noise, so please drop in a comment if you have Jest tooling that can do that. For now, I can commit:
Tell a better story when the test runs.
I think that’s it for refactoring at this stage. So let’s move onto Dave’s next test:
So, this is where TDDAIYMI really kicks us hard. To get this to pass I’m going to have to somehow represent in the test that I scan an “A” and get a price of 50. Well, this is still TDD, so I should be getting this to pass without doing much — if indeed, any — design in my head. How about this:
This feels like it tells the story intended by the test: We begin with a zero total, then we scan an “A” and then the total price becomes 50. (The const
itemsScanned is highlighted because vim is helpfully telling me that it isn’t used.)
All tests pass:
So I commit:
Get the correct price when scanning an A.
Now it’s time to refactor, and this is the moment when TDDAIYMI pays off. This code now violates the 4 rules — in particular the Once And Only Once rule. Next time we’ll fix that — but in the meantime drop me a comment: what would you refactor now, and why?
Things to try
Take half an hour to do a (different) kata using the TDDAIYMI style. What did you learn about your usual programming style? Or about how much “design” you do without consciously calling it out?
Thanks for reading Habitable Code! Subscribe to receive new posts and support my work.
If you wish to configure Jest's reports, you may have a look at this list of reporters: https://github.com/jest-community/awesome-jest/blob/main/README.md#reporters
IMO, it's not necessary at this point. The default reporter is doing his job well.
Thanks for documenting your journey Kevin, it's very interesting to follow indeed 😉