On-site TDD
One of the recurring questions in TDD workshops is "How do I test private methods?“. My usual answer is worded along the following lines: "You don’t. Any private method should be tested through the public interface. If you think the private method is complex enough for deserving its own test(s), extract it to a public place and test it there.“ I still think this is the best general answer I can give, however, I recently discovered a set of situations that I handle differently.
Imagine yourself trying to implement some non-trivial solution to a problem. By "non-trivial“ I mean that the necessary algorithm is complicated enough so that you cannot oversee all intermediate steps and decision points in your mind alone. You’re tackling the problem step by step — pardon-me — test by test. At some point, there will be one test that forces you to implement at least part of the algorithm. The tests you created will eventually be sufficient to cover the logic, but they are not fine-grained enough to let you grow the solution in tiny, controllable steps.
Enter On-site TDD. This technique runs a few TDD cycles "on-site“ meaning "directly inside the production code“. The goal is to enable finer-grained TDD without the overhead of having to (temporarily) extract an implementation detail. Let’s demonstrate the technique with an example: Our task is to encrypt a text using columnar transposition: You take a String text and an Integer key, split the text into lines of length key and then assemble the text by columns — top-down and left to right. Here is the encryption table for "the battle will start at daybreak
“ with key 7. Ignoring all spaces the resulting cipher text is "tltaheayewrbbitralaetltatsdk
“:
In order to reduce the usual testing framework noise I’ll go with a simple Groovy script for both test code and production code. I’ll leave it to the astute reader to imagine test classes and production classes.
We start with the trivial case:
assert encrypt('ab', 2) == 'ab'
def encrypt(text, key) {
text
}
And proceed to an example that requires to really work with the input text:
assert encrypt('abcdef', 3) == 'adbecf'
which can trivially be fulfilled like this:
def encrypt(text, key) {
if (text == 'abcdef' && key == 3) {
return 'adbecf'
}
text
}
At this point we have several options:
- Adding another example and then try to come up with the implementation all at once. This is called triangulation.
- Trying to write a real implementation now and tweak it till the tests pass.
- Evolving the algorithm and tests step by step -- inside the production code. This is what I will show here...
We focus on the non-trivial branch and specify the first piece on our way to a working algorithm, which is splitting the text into individual characters:
def encrypt(text, key) {
if (text == 'abcdef' && key == 3) {
def chars
assert chars == ['a', 'b', 'c', 'd', 'e', 'f']
return 'adbecf'
}
text
}
Now our tests will fail, but we can easily fix this:
def encrypt(text, key) {
if (text == 'abcdef' && key == 3) {
def chars = <strong>text.toList()</strong>
assert chars == ['a', 'b', 'c', 'd', 'e', 'f']
return 'adbecf'
}
text
}
Next, we add an assertion for splitting the chars into lines of length 3:
def encrypt(text, key) {
if (text == 'abcdef' && key == 3) {
def chars = text.toList()
assert chars == ['a', 'b', 'c', 'd', 'e', 'f']
def lines
assert lines == [['a', 'b', 'c'], ['d', 'e', 'f']]
return 'adbecf'
}
text
}
And again, fixing the broken test is just a matter of looking up the correct method in Groovy’s Development Kit:
def encrypt(text, key) {
if (text == 'abcdef' && key == 3) {
def chars = text.toList()
assert chars == ['a', 'b', 'c', 'd', 'e', 'f']
def lines = <strong>chars.collate(key)</strong>
assert lines == [['a', 'b', 'c'], ['d', 'e', 'f']]
return 'adbecf'
}
text
}
Let’s speed up a bit. Here come assertion and implementation for converting the lines to columns:
def encrypt(text, key) {
if (text == 'abcdef' && key == 3) {
def chars = text.toList()
assert chars == ['a', 'b', 'c', 'd', 'e', 'f']
def lines = chars.collate(key)
assert lines == [['a', 'b', 'c'], ['d', 'e', 'f']]
def columns = lines.transpose()
assert columns == [['a', 'd'], ['b', 'e'], ['c', 'f']]
return 'adbecf'
}
text
}
The last but one step is flattening the columns:
def encrypt(text, key) {
if (text == 'abcdef' && key == 3) {
def chars = text.toList()
assert chars == ['a', 'b', 'c', 'd', 'e', 'f']
def lines = chars.collate(key)
assert lines == [['a', 'b', 'c'], ['d', 'e', 'f']]
def columns = lines.transpose()
assert columns == [['a', 'd'], ['b', 'e'], ['c', 'f']]
def cryptedChars = columns.flatten()
assert cryptedChars == ['a', 'd', 'b', 'e', 'c', 'f']
return 'adbecf'
}
text
}
What remains is assembling the letters into a string:
def encrypt(text, key) {
if (text == 'abcdef' && key == 3) {
def chars = text.toList()
assert chars == ['a', 'b', 'c', 'd', 'e', 'f']
def lines = chars.collate(key)
assert lines == [['a', 'b', 'c'], ['d', 'e', 'f']]
def columns = lines.transpose()
assert columns == [['a', 'd'], ['b', 'e'], ['c', 'f']]
def cryptedChars = columns.flatten()
assert cryptedChars == ['a', 'd', 'b', 'e', 'c', 'f']
def result = cryptedChars.join('')
assert result == 'adbecf'
return result
}
text
}
Now we can get rid of asserts and the special-case branch:
assert encrypt('ab', 2) == 'ab'
assert encrypt('abcdef', 3) == 'adbecf'
def encrypt(text, key) {
def chars = text.toList()
def lines = chars.collate(key)
def columns = lines.transpose()
def encryptedChars = columns.flatten()
def result = encryptedChars.join('')
return result
}
Et voilà, we arrived at a working algorithm in tiny steps; much tinier than would have been possible by sticking to assertions within the test class only. Of course, you should choose the step size according to your knowledge of language and domain. When in doubt, take a smaller step to stay in full control.
Most of the times I am happy with deleting the assertions now that they've fulfilled their duty. When I feel they should stick around after all, I will make real tests out of them by extracting the logic into a class of its own and moving the assertions to a test class - an existing or a new one, depending on where I extracted the code to.
One precondition for doing On-site TDD is the ability to write assertions - or something to the same effect - inside your production code without thereby creating a dependency on the test framework. If you cannot do that, there is another way of achieving something similar: First, move the parts of the production code you want to evolve over to your test class. Second, go about implementing your solution in the way I’ve demonstrated above. Last, move the code back to the production class. This is - by the way - what you’re supposed to do when practicing "TDD as if you meant it“.
As always, feedback and criticism is more than welcome!
Update 1: REPL
As some of the commenters on twitter mentioned: When you're lucky enough to use a language with a decent REPL, most (if not all) of the On-site TDD steps can be done there. When using a REPL with inline evaluations (e.g. light table) you might even forgo the assertions completely since you do see the values of the temp vars anyway.
Update 2: Outside-In
One of the commenters remarked that On-site TDD looks like mostly useful for inside-out (or bottom-up) TDD. So far I have been using it exclusively in inside-out situations. Trying to imagine useful outside-in scenarios is not straightforward - at least not to me. As far as my experiments went, using a dependency was never complicated enough that On-site TDD seemed necessary. But hey, if YOU come up with a good example, PLEASE let me know.
Leave a comment