Overview

TDD is a programming technique that was created by Kent Beck.  In order to write code using TDD you’ll need to follow the three steps below in a cycle until your code is complete.

  1. Red – Write just enough code to make a failing test
  2. Green – Write just enough implementation code to make the test pass
  3. Refactor

TDD is something that you can learn in an afternoon but takes a substantial amount of practice to master.

Why Should I Use TDD?

  1. It ensures the code written is testable.
  2. It provides a fast feedback loop.
  3. It reduces time spent debugging.
  4. It forces fore thought of interface design.
  5. TDD builds out a comprehensive automated test suite.  This enables refactoring to occur without fear of breaking the system.

Implementing A Stack Using TDD

Objective

  1. Using TDD implement a stack (dont’ use the Java stack class) with the methods defined below.
package com.stevekatra.tdd.stack;

public interface IStack {
    int size();
    void push(int i);
    int pop();
    int peek();
    boolean isEmpty();
}

Let’s Get Started

Implement Size

Let’s write our first test!

package com.stevekatra.tdd.stack;

import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class StackTest {

    @Test
    public void sizeOfStackShouldBeZero() {
        Stack stack = new Stack();
        assertThat("New stack should be empty",stack.size(),is(0));
    }
}

Let’s execute the first test.  Compilation errors! Egad!  This is what we wanted to happen.  This proves that a Stack is not already implemented or imported.  This is the RED step from the cycle above.

Let’s fix this test and write just enough code to make this test pass.

package com.stevekatra.tdd.stack;

public class Stack {
    public int size() {
        return 0;
    }
}

We are in the GREEN state with all tests passing.  Now lets look for opportunities to refactor.  None were identified and this completes our first cycle.

Implement Push

Let’s write our second test for the push method.

@Test
public void pushOneValueOnStack() {
    Stack stack = new Stack();
    stack.push(1);
    assertThat("Size should be one after pushing one item onto the stack.", stack.size(), is(1));
}

Let’s rerun our tests.  Another failing test.  This is what we expected to happen in our cycle.

Let’s add the missing implementation for the push method to fix this test.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }
}

Let’s rerun our tests and they pass!  This means we are the GREEN phase of the cycle.

Let’s look for opportunities to refactor the tests and implementation in the REFACTOR phase.  In the tests there is no need to repeat the Stack declaration and initialization with each test.  Let’s extract that out into a setup method and see see if our tests still pass.

package com.stevekatra.tdd.stack;

import org.junit.Before;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class StackTest {

    private Stack stack;

    @Before
    public void setUp() throws Exception {
        stack = new Stack();
    }

    @Test
    public void sizeOfStackShouldBeZero() {
        assertThat("New stack should be empty",stack.size(),is(0));
    }

    @Test
    public void pushOneValueOnStack() {
        stack.push(1);
        assertThat("Size should be one after pushing one item onto the stack.", stack.size(), is(1));
    }
}

Everything still passes and no opportunities to refactor in the implementation code were identified.

Let’s add an additional test case for pushing two items and checking the size of the stack to validate the implementation.

@Test
public void pushTwoValuesOnStack() {
    stack.push(1);
    stack.push(2);
    assertThat("Size should be two after pushing one item onto the stack.", stack.size(), is(2));
}

Let’s rerun the tests and see if we have work to do.

Everything still passes so there is no need to add additional implementation code.

Implement Pop

Let’s added a test for the pop method and rerun the tests.

@Test
public void pushOnePopOneValueOnStack() {
    stack.push(1);
    int popReturnValue = stack.pop();
    assertThat("Size should be zero after pushing and popping one item from the stack.", stack.size(), is(0));
    assertThat("1 was pushed on the stack and 1 should be returned when calling pop.", popReturnValue, is(1));
}

The failing test means we are the RED phase of the cycle and need to add the implementation for pop to fix the tests.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        return 0;
    }
}

Let’s rerun the tests to see if we are done.

We are still in the RED phase with a failing test due to assert error.  Pushing one onto the stack and calling Pop() produced zero but was expected to be one.  Let’s add some additional implementation code to fix this test.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        return stack.remove(stack.size()-1);
    }
}

Let’s rerun the tests to see if they are all passing.

Now that we are in the GREEN phase lets look for opportunities to REFACTOR.  None were identified.

Implement Peek

Let’s add the next test for peek().

@Test
public void pushOneAndPeek() {
    stack.push(1);
    assertThat("1 was pushed on the stack and 1 should be returned when calling peek.", stack.peek(), is(1));
}

Let’s rerun the tests.

We are in the RED phase with failing tests.  Let’s add some implementation to fix this test.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        return stack.remove(stack.size()-1);
    }

    public int peek() {
        return 0;
    }
}

Let’s rerun the tests.

We are in the RED phase with failing tests.  Let’s add some implementation to fix this test.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        return stack.remove(stack.size()-1);
    }

    public int peek() {
        return stack.get(stack.size()-1);
    }
}

Let’s rerun the tests.

Now that we are in the GREEN phase lets look for opportunities to REFACTOR.  None were identified.

Let’s  think about a negative scenario.  What happens if we peek before we add anything to the stack?  I’d expect to see receive a Runtime exception.

@Test
public void peekOnEmptyStack() {
    stack.peek();
}

Let’s rerun the tests.

We receive an ArrayIndexOutOfBoundsException.  This is not what we want to provide to the end users of stack.  Let’s check for an EmptyStackException instead which is more descriptive of the situation.

@Test(expected = EmptyStackException.class)
public void peekOnEmptyStack() {
    stack.peek();
}

Let’s rerun the tests.

We are still in the RED phase and need to defined EmptyStackException.

package com.stevekatra.tdd.stack;

public class EmptyStackException extends RuntimeException {
}

We also need to add code to throw the EmptyStackException when calling peek on an empty stack.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        return stack.remove(stack.size()-1);
    }

    public int peek() {
        if(stack.size() == 0)
            throw new EmptyStackException();
        return stack.get(stack.size()-1);
    }
}

Let’s see if the tests pass.

The tests pass!  There are no identified opportunities to refactor.  Let’s add the same boundary test for pop();

@Test(expected = EmptyStackException.class)
public void popOnEmptyStack() {
    stack.pop();
}

Let’s rerun the tests.

We receive an ArrayIndexOutOfBoundsException again.  We expected to receive an EmptyStackException.  Since we are in a RED state we need to add additional implementation.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        if(stack.size() == 0)
            throw new EmptyStackException();
        return stack.remove(stack.size()-1);
    }

    public int peek() {
        if(stack.size() == 0)
            throw new EmptyStackException();
        return stack.get(stack.size()-1);
    }
}

Let’s see if the tests pass.

With all of the tests passing we are now in a GREEN state.  I ended up copy pasting a few lines of validation for the EmptyStackException.  That means we can enter the REFACTOR phase and extract some methods.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        validateStackIsNotEmpty();
        return stack.remove(stack.size()-1);
    }

    private void validateStackIsNotEmpty() {
        if (stack.size() == 0)
            throw new EmptyStackException();
    }

    public int peek() {
        validateStackIsNotEmpty();
        return stack.get(stack.size()-1);
    }
}

Let’s check if the tests still pass.

Implement isEmpty

Great!  Let’s add the a test for isEmpty method.

@Test
public void emptyStack() {
    assertThat("isEmpty method reports true for an empty stack", stack.isEmpty(), is(true));
}

Let’s check if the tests pass.

Now that we are in a RED state let’s add the implementation to make this test pass.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        validateStackIsNotEmpty();
        return stack.remove(stack.size()-1);
    }

    private void validateStackIsNotEmpty() {
        if (stack.size() == 0)
            throw new EmptyStackException();
    }

    public int peek() {
        validateStackIsNotEmpty();
        return stack.get(stack.size()-1);
    }

    public boolean isEmpty() {
        return true;
    }
}

Let’s see if the tests pass.

The tests pass but the isEmpty method has a hard coded value.  That doesn’t feel right but the tests pass.  Lets add another test to discover the issue.

@Test
public void populatedStack() {
    stack.push(1);
    assertThat("isEmpty should report false if there are items on the stack", stack.isEmpty(), is(false));
}

Let’s see if the tests pass.

The test did not pass and we are in a RED state.  Let’s add the required implementation.

package com.stevekatra.tdd.stack;

import java.util.ArrayList;
import java.util.List;

public class Stack {
    List<Integer> stack = new ArrayList<Integer>();

    public int size() {
        return stack.size();
    }

    public void push(int i) {
        stack.add(i);
    }

    public int pop() {
        validateStackIsNotEmpty();
        return stack.remove(stack.size()-1);
    }

    private void validateStackIsNotEmpty() {
        if (stack.size() == 0)
            throw new EmptyStackException();
    }

    public int peek() {
        validateStackIsNotEmpty();
        return stack.get(stack.size()-1);
    }

    public boolean isEmpty() {
        return stack.size() == 0;
    }
}

Let’s check the tests.

All of the tests pass and there are no obvious refactors.  That concludes the objective.

Git Repository With Final Code

Tell Me About Your Experience With Test Driven Development

I’d like to hear about your experience with TDD and any feedback you have about this post!