Why one unit test is never enough
In 2005 I was about to graduate in Computer Science when I was persuaded to join a remote amateur team that was experimenting with using innovative extreme programming techniques to create a clone of Super Puzzle Fighter.
This primarily didactical project, named Diamond Crush, introduced me to remote working, agile, continuous integration, pair programming, and above all TDD.
To this day I believe it to be one of the most enriching experiences I ever had and since then I’ve been using TDD in my projects as much as possible.
Test-Driven incorporates Test-First
The project’s had strict policies, among others one was that every line of code had to be unit tested, for each foreseeable outcome (i.e. 100% code coverage was not enough).
To help that, our pair programming sessions were organised so that one developer would write the simplest build-breaking test towards the feature we were working on, the other would then implement the minimum amount of code necessary to make the build pass again. Then both developers would review the code and see if it could be further simplified without breaking the build.
With this approach, every functionality would start with absolutely naïve tests, such as:
# Test
def test_doubles_one
assert int_double(0) == 0
end# Implementation
def int_double(value)
return 0
end
This technique is called triangulation, and at first it may look awkward but you’d be surprised by how much thinking had to take place to write that simple implementation: deciding the name of the function, the number and names of the parameters, and depending on the language it could require several lines of code to get the build running.
The main benefit of this approach, is that it forces the developer to think in smaller steps, and it helps to figure the edge cases and different usages incrementally. Because one of the two developers’ role is focused on breaking the build, they will use their creative power to think of all the previously mentioned foreseeable outcomes. Eventually, I learned to do that myself, and to wear the build breaker hat, then the fixer hat in cycles when I work on a more complicated feature.
I wasn’t too sure about this approach when I first saw it, but one day I had to work on a more complicated feature, and I had no idea how to solve the problem, but using this method I figured out what I initially thought would be impossible.
One of the game mechanics that I needed to implement, was to detect when a set of blocks formed a rectangle, to detect the largest possible, and then substitute the gems with a “big gem” covering the same area. I spiked the feature with different complicated algorithms, but then pairing with a much more experienced developer, we approached the problem starting by scanning the grid to detect a simple 2x2 square and then trying to expand it to all directions until possible. This actually worked and it ended up being a very simple algorithm.
Something I see very often is that developers only test one happy scenario for any given function, or perhaps only one error and one successful case; they are not enough.
Say that I was working on an image editor and I wanted to add the “draw a rectangle” feature. I would first create a test to create a 1x1 pixel rectangle (or a test that says you can’t create a degenerate rectangle), then make the test pass. Then I would start building up my feature by creating more interesting cases with a 2x1 rectangle, make it pass, then a 1x2, make it pass, and finally a 2x2 and make it pass again. The next steps would be a 3x3 rectangle to ensure only the outline is being drawn, then I would probably start adding tests for drawing starting from different positions (top-left, bottom-right) and then finally the error cases (can’t draw outside of the image). It doesn’t take that much time to write tests, and it helps to document the code.
Starting from the simplest usage and building up helps to cover all the possible edge cases.
I don’t think one test would have been enough here, nor two…
Wrap it up
By all means, this project was didactical and not necessarily the most optimal approach to get things done (TM). Admittedly you wouldn’t want to triangulate all the time, or perhaps triangulate more effectively to reduce the number of tests.
The project’s code can be found here: https://github.com/Zeneixe/diamonds