I’ve just read Kent Beck’s TDD by example book and I’d like to share some of the insights I got from it.

Let’s do a bit of TDD..

I was thinking we could do a small exercise. We are going to create a range function that returns an array of numbers. This function will take as input the first and last numbers in that sequence.

1  to 10  -> [1,2,3,4,5,6,7,8,9,10]
3  to 7   -> [3,4,5,6,7]
23 to 23  -> [23]

We are going to use the universal language for it: JavaScript. So everybody can follow it. ;)

Lets write our first test:

describe('range', () => {
  it('should return a range of numbers', () => {
    expect(range(3, 7)).toEqual([3, 4, 5, 6, 7]);
  });
});

We run the test and we get a failing test. We are in the red! Let’s get to green..

In the book, Kent Beck mentions 3 different ways to approach the failing test.

Fake It (‘Til You Make It)

The first way is to just fake the implementation. We want to get quickly to the green. This is the simplest thing that could possibly work.

So let’s just copy the array from the test and return it inside a newly created range function.

function range(from, to) {
  return [3, 4, 5, 6, 7];
}

Good. Now we have a green bar! We are in a safe place. We can start removing duplication.

The first obvious duplication is between the test and the production code - the hard-coded array [3, 4, 5, 6, 7]. We want to get rid of it inside the range function.

If you pay attention closely you will see there is a subtle duplication between the elements of the array and the first argument.

The first element can be replace with from:

function range(from, to) {
  return [from, 4, 5, 6, 7];
}

We run the tests and they are still green. We will keep running the tests with every code change.

The rest of the elements are just the first element plus 1, 2, 3 and 4:

function range(from, to) {
  return [from, from + 1, from + 2, from + 3, from +4];
}

With this refactoring we can see clearly the underlying duplication and we can infer the missing abstraction:. We have an array where each element is from + index of the array.

Let’s remove that duplication. We could use a for loop:

function range(from, to) {
  let result = [];
  for(let index = 0; index <= 4; index ++) {
    result.push(from + index);
  }
  return result;
}

Now.. that 4 is bugging me. What does it really mean? Let’s clarify its intent.

function range(from, to) {
  let result = [];
  const numOfElements = 4;
  for(let index = 0; index <= numOfElements; index ++) {
    result.push(from + index);
  }
  return result;
}

The 4 represents the number of elements in the array.

Now that we know that, we just realise that it is the same as the difference between from and to.

function range(from, to) {
  let result = [];
  const numOfElements = to - from;
  for(let index = 0; index <= numOfElements; index ++) {
    result.push(from + index);
  }
  return result;
}

I am tempted to in-line the variable numOfElements. But thinking about it.. I’ve just realised that the loop could be simplified:

function range(from, to) {
  let result = [];
  for(let current = from; current <= to; current ++) {
    result.push(current);
  }
  return result;
}

Voilà! No more duplication. The method is clean :)

With Fake It (‘Til You Make It) we wrote just one test with a single assertion, we faked it and then slowly remove the duplication until we got to a more generic code.

Triangulate

Sometimes Fake It (‘Til You Make It) feels like a big step. It could be that we don’t really know what’s the code design we are after or that we can’t clearly see the duplication. In those cases we will need to take smaller steps and slowly triangulate until we get to the desired implementation.

In order to do that, we will need to start with a simpler test:

describe('range', () => {
  it('should return a range of numbers', () => {
    expect(range(3, 3)).toEqual([3]);
  });
});

Like before, we do the simplest thing that could possibly work. We fake it:

function range(from, to) {
  return [3];
}

We are not sure if it’s too early to start refactoring, we need more data. So we write another test to force us to extend the implementation:

describe('range', () => {
  it('should return a range of numers', () => {
    expect(range(3, 3)).toEqual([3]);
    expect(range(3, 4)).toEqual([3, 4]);
  });
});

Simplest thing to make this work is to add an if for the new case:

function range(from, to) {
  if (to === 4)
    return [3, 4];
  return [3];
}

At this point, we can start removing a bit of duplication:

function range(from, to) {
  if (to === 4)
    return [3].concat([4]);
  return [3];
}
function range(from, to) {
  if (to === 4)
    return range(3,3).concat([4]);
  return [3];
}

We could keep refactoring. Those 4s and 3s are everywhere!! But we are still not sure if they mean the same, so we will wait. We don’t want to extract the wrong abstraction.

We need more sample data. That means more tests:

describe('range', () => {
  it('should return a range of numbers', () => {
    expect(range(3, 3)).toEqual([3]);
    expect(range(3, 4)).toEqual([3, 4]);
    expect(range(3, 5)).toEqual([3, 4, 5]);
  });
});

Let’s add another horrible if branch to the code:

function range(from, to) {
  if (to === 5)
    return range(3,4).concat([5]);
  if (to === 4)
    return range(3,3).concat([4]);
  return [3];
}

Ok, now the duplication is more obvious. Let’s start removing it slowly:

function range(from, to) {
  if (to === 5)
    return range(3,to-1).concat([to]);
  if (to === 4)
    return range(3,to-1).concat([to]);
  return [3];
}
function range(from, to) {
  if (to === 4 || to === 5)
    return range(3,to-1).concat([to]);
  return [3];
}

We can generalise that to === 4 || to === 5 to to > 3. We could write another test to drive the change or just be brave and do the change. We are feeling brave today so will just change it:

function range(from, to) {
  if (to > 3)
    return range(3,to-1).concat([to]);
  return [3];
}

OK, now it’s clear what that 3 represents :

function range(from, to) {
  if (to > from)
    return range(from,to-1).concat([to]);
  return [from];
}

Voilà! :)

As we can see, Triangulatation is more conservative than Fake It (‘Til You Make it). Instead of having a single sample data in the test we rely in multiple samples. We introduced these samples incrementally while slowly extended our code. We had to make a conscious decision when picking the samples, started with what we thought would drive the simplest implementation and added samples that we thought would slowly make the implementation more generic. The tests got more specific while the code got more generic.

Obvious Implementation

Sometimes you already now the answer to the problem. Maybe you already solved the same problem before, maybe the problem is not that hard or maybe you have seen the answer in stack overflow.. ;)

If that’s the case, why bother with tiny steps? Let’s write the obvious implementation and get it over and done with!

function range(from, to) {
  return Array.from(new Array(to - from + 1), (x, index) => from + index);
}

Conclusion

There are diffent “speeds” that you can apply to your TDD approach and each of them has its trade-offs. Triangulate is a safe way to slowly drive your implementation, Obvious Implementation is fast but risky and Fake It(‘Till you make it) is something in between. None is the best way to test drive your code. You have to use each of them depending on the problem at hand. Sometimes you will start taking big steps and realise that you are getting nowhere. In that case you will have to stop, go back and restart with a less riskier approach. Other times you will start slowly and realise you can speed up.

I love TDD and I have been doing TDD for many years, but I hadn’t read Kent Beck’s book till now (shame on me!). Through the years I learnt TDD reading blogposts, parining at work or events, I’ve watched videos or done pluralsight courses, but I hadn’t read the book. The thing is that most of this material on the internet focuses heavily in triangulation. Because of this, most of the time I’ve been taking tiny steps when doing TDD. After reading the book I understand that taking tiny steps it’s not always the most efficient way and that I should take a more pragmatic approach and set the appropriate speed for each case.