Today in class I got to understand, for the first time, what I define as “the simplest thing that could possibly work” when doing TDD. Many people struggle with the idea of simplicity. the idea of doing things to the point of “micro-increments” to the code, that will then help in driving out new tests that you had not thoguht about, is really tough.
what’s missing is a simple set of rules you can check your implementation against.
here’s a set of rules to serve as the basis for this discussion. I’m sure there are more, and i’m sure this will change over time, as defining this “simplicity pattern” is a rather new thing for me. I have not seen this done elsewhere.
the situation:
- You had a failing test
- You went to the production code, and made the test (and all other tests) pass, in the simplest way you thought possible.
- But was it really the simplest way?
look at the code you just wrote in your production code and ask yourself the following:
“Can I implement the same solution in a way that is ..”
- “.. More Hard coded ..”
- “.. Closer to the beginning of the method i wrote it in.. “
- “.. Less indented (in as less “scopes” as possible like “if”s, loops, try-catch ..”
- “.. shorter (literally less characters to write) yet still readable ..”
“… and still make all the tests pass?”
if the answer to one of these is “yes” then do that, and see all the tests still passing.
the trick is this: by doing this, you’ve implemented the same feature (or part of feature) and yet you knowthat the feature is not complete yet. Something is missing. it might be too hard coded now, or not in the correct scope or it’s missing a check.. but all the tests are still passing, so you can’t change the code now(in tdd you can only change production code functionality when you have a failing test)!
what do you have to do to be able to change the code?
Add a test that proves that something is missing in the code!
sometimes, you might discover that the simpler way is actually a good enough solution that does not need changing, or any more tests.
either case, you’ve won – you either got a simpler solution to the same problem, or you discovered a test that was missing.
Example:
say, that, you had a new requirement to add a call to a logger when a login succeeds for a user. I won’t show the test that failed, just the code that you might have written to make the test pass:
You added one line of code, that calls a logger with the string containing the user name and a short message.
Now, let’s see how we can make this implementation simpler, without breaking any tests:
1. can I make it “.. More Hard coded ..” without breaking tests?
YES (I only have one test)
assuming the test was trying to log in with a user names “testuser” i can make the test pass by hardcoding the user name. this would force me to write another test that sends in a different user name to the method, that proves this cannot be a hard coded string.
2. can I write it “.. Closer to the beginning of the method i wrote it in.. “
YES. Assuming there is no test that assume logger is called otherwise from this method, I can do this easily without breaking the test. I’ll just move the code up to the start of the method:
3. can i make it “.. Less indented (in as less “scopes” as possible like “if”s, loops, try-catch ..”
YES. I already did, in the previous step.
4. can I make it “.. shorter (literally less characters to write) yet still readable ..”
doesn’t look like it.
now, I have this code, and i know it is not good enough. I need to prove that using tests before I can change it, which forces me to think of two new tests: one that logs in with a different user name, and another that makes sure the logger is not called unless the login is successful.
Are these tests valuable?
I’m not sure. Do we really need a double test with two different values? I’m partial to this.
Do we realy need a test that proves something doesn’t happen? not crazy about it. it feels a but like over specification – why should i care if the logger is called or not in a place I don’t care about in terms of functionality? that test would break the second someone would add logging there, which is obviously needed, so it might just be a test waiting to be deleted.
on the other hand…
doing things so incrementally is a god way to teach small, incremental steps. as students become proficient, they can choose which increments to skip, but these small steps are good for “muscle memory” during kata training.
should i really forgo these increments (therby loostening up the need to “micro-increment” and giving up muscle learning) in favor of teaching types of tests that arn’t needed? or is muscle memory important enough to “sacrifice” some robustness in a few tests (which is also very very important).
which value is more important? micro-increments? or recognizing un-needed tests? which one is easier to learn later? it seems micro-increments (the way i teach) require writing some tests that may feel un-needed. but do I really want to let go of forcing this very powerful habit?