Unit Conversations
Writing characterization tests interactively
There are many ways to look at unit testing, but one of my favorites is to see it as an attempt to have a REPL in languages that don't have one.
When you have a REPL, you can call a function and learn how it works interactively. You get immediate feedback—you can have a conversation with it. In dynamically-typed languages this is easy. Most of them have a REPL. In statically-typed languages it is harder. The compiler needs to perform its checking. In the worst cases it has to check across a transitive set of dependencies. Tool makers often don't even think about writing a REPL for languages like these. The conversations that you would be able to have with your code wouldn't be immediate—there would be too many pauses.
Despite the similarities, there's one key difference between a REPL and a set of tests. Tests are encapsulated, independently executable pieces of history—the history of a precise interaction with the thing that you are testing.
We could try to create tests from our REPL conversations by copying and pasting into test files but it's awkward and tedious. Thankfully, we're programmers. We know what to do—automate.
I wrote a little proof of concept in Ruby. It's called C11R (a numeronym for "Characterizer").
The idea was to have a little tool that helps us interactively write unit-level characterization tests for existing code. The tool is conversational. We write lines of code to set up our environment, run functions with specific parameters and see what the results are. If we like the results, we issue a command to save what we've done as a test.
Here is a simple example. When we start c11r, we can ask it questions:
> ask 1 + 1
c11r will respond with:
- 2
Easy enough, right?
If we like 2 as an answer (and we should), we use the push command and c11r outputs a test for what we've learned.
> push ruby seems to give us 2 when we want it
Here is c11r's output (in a separate file):
test "ruby seems to give us 2 when we want it"
assert_eq(2, 1 + 1)
end
At this point, you're probably wondering how we can do something more complicated.
For complicated things, we use the fix command.
> fix new
> fix add a = A.new
> fix add b = B.new
> ask a.run_with(3) + b.run_with(2)
- 12
Here we are using fix new to create a fixture (only one can be active at a time). The fix add command appends lines to the empty fixture, and ask does exactly what you saw above, yielding the answer 12.
The nice thing about fixtures is that they persistent until we run fix new again.
Let's write more tests.
> fix new
> fix add a = A.new
> fix add b = B.new
> ask a.run_with(3) + b.run_with(2)
- 12
> push runners off by one yield simple value
> ask a.run_with(3) + b.run_with(5)
- 24
> push runners off by more than one do level jump
If we are ever unclear about what the fixture contains we can run fix to see it.
> fix
- a = A.new; b = B.new
Writing tests becomes as easy as having a conversation.
It's easy to imagine this doing this sort of thing as an IDE plugin, with text boxes to hold the fixture and the ask line. I'm actually surprised that no one is doing this. It's just an extra little thing that can make characterization testing easier.
You can find the proof of concept code here. Please get in touch if you expand upon it and take it further.