The Right Tests in the Wrong Order

The Right Tests in the Wrong Order

Mike Jansen
Mike Jansen

July 18, 2013

For those new to Test Driven Development, understanding how to take small, incremental steps when writing your tests can be a confounding practice. Even the most basic of problems can lead a developer down a frustrating path. It is possible to write all of the "right" tests, but if they are in the wrong order, the benefits of TDD can decrease significantly.

Adventures With An Integer Stack

I recently had the opportunity to train some aspiring craftsmen in Test Driven Development. The exposure level coming in was minimal - a few days learning about TDD in a previous course, and even a few weeks trying to practice it on their own. After all this though, they were still essentially new to the practice.

We discussed the basics of TDD for about an hour, including the Three Rules of TDD. Following that, the group saw a demo of the Prime Factors kata. In the demo, we stressed taking the smallest step possible when test driving your code. This is straightforward in prime factors - start with a test case for 1, then 2, then 3, then 4, and so on. By the time you reach 9, it is trivial code change to complete the algorithm.

(For reference, here's the end result in Java that was shown: Prime Factors in Java.

This example from Uncle Bob is a very close approximation to the demo given, except in Ruby.)

Given this introduction, we set the group to the task of test driving a basic Stack implementation in Java, with the following interface:

+ push(int)
+ pop(): int
+ peek(): int
+ size(): int

Given this class was also introducing Java, we had an additional constraint of using an int array of size 10 for storing the integers, since Collections had not been introduced.

Also, at this point we weren't worried about things like pushing when you were out of space, or popping when there were no values, just to keep the example simple.

Incremental Steps

Here are the tests as they were implemented, in order:


public class IntegerStackTest {

				private Stack stack;

				@Before
				public void setup() {
								stack = new IntegerStack();
				}

				@Test
				public void startsEmpty() {
								assertEquals(0, stack.size());
				}

				@Test
				public void sizeIncreasesWithPush() {
								stack.push(50);
								assertEquals(1, stack.size());
				}

				@Test
				public void lastInFirstOutWhenPopping() {
								stack.push(50);
								assertEquals(50, stack.pop());
				}

				@Test
				public void canPeekAtLastValueAdded() {
								stack.push(50);
								assertEquals(50, stack.peek());
				}
}

As you might be able to tell from these tests, the incremental approach here was to add tests that build out the basic functionality of the public API. By itself, this is a fairly reasonable approach, and every one of these tests is a valid test.

However, when I looked at the code, I saw an...interesting implementation. Given our discussion of the three Rules of TDD, the least amount of code needed to make the tests pass was written, and the implementation looked like:

public class IntegerStack {

				private int value;

				public int size() {
								if (value != 0)
												return 1;
								else
												return 0;
				}

				public void push(int value) {
								this.value = value;
				}

				public int pop() {
								return value;
				}

				public int peek() {
								return this.value;
				}

}

The tests all passed, but the next step of supporting multiple pushes with an Array object was going to change the implementation of every single method (not to mention the implementation of .size() was questionable). In this small class, this is a relatively large change and it breaks the short red/green/refactor feedback loop.

Ordering Your Tests

It's this kind of start with TDD that can be frustrating. By distilling the introduction down to the basic rules and common examples of TDD, the idea of taking small, incremental steps can get distorted. While it may have been easy to write the tests in that order, the code evolved to a place where relatively significant changes were needed to advance.

This brings me back to my point - TDD is hard to learn. Despite the best intentions of the trainees, they essentially ended up in a place where they needed to implement all of the Stack functionality in one go.

At this point, I advised them to back up and implement the behavior of .size() first via .push() and .pop(), and don't worry about what .pop() or .peek() return until later. That sequence looks like this:

public class IntegerStackTest {

				private Stack stack;

				@Before
				public void setup() {
								stack = new IntegerStack(10);
				}

				@Test
				public void startsEmpty() {
								assertEquals(0, stack.size());
				}

				@Test
				public void sizeIncreasesWithPush() {
								stack.push(50);
								assertEquals(1, stack.size());
				}

				@Test
				public void sizeIncreasesWithPushes() {
								stack.push(50);
								stack.push(100);
								assertEquals(2, stack.size());
				}

				@Test
				public void sizeDecreasesWithPop() {
								stack.push(50);
								stack.pop();
								assertEquals(0, stack.size());
				}

				@Test
				public void lastInFirstOutWhenPopping() {
								stack.push(50);
								assertEquals(50, stack.pop());
				}

				@Test
				public void popsMultipleValues() {
								stack.push(50);
								stack.push(100);
								assertEquals(100, stack.pop());
								assertEquals(50, stack.pop());
				}

				@Test
				public void canPeekAtLastValueAdded() {
								stack.push(23);
								stack.push(50);
								assertEquals(50, stack.peek());
				}

}

By tackling the behavior of .size() first, you can implement the behavior of .push() and .pop() incrementally, separating the size component from the actual return of values. Given this approach, the red/green/refactor loop stayed short, and the trainees were able to quickly finish the exercise.

Listen To The Feedback

TDD works best when the feedback cycle is short. For those new (and even those that are experienced), remember to listen to this feedback loop. If you find yourself implementing large swaths of code to keep your test suite green, you may have taken a wrong turn at some point.

Don't be afraid to back up and try another approach. This type of practice is invaluable and you'll see the benefits when test driving production code.