SWE 430: Example 1.1 --- Test-Driven Development with Automated Unit Testing

This lesson will demonstrate test-driven development (TDD) and automated unit testing by applying the methods to develop a simple command and control (C2) application that could be used as a component in support of a larger C2 application.

Problem Description

The application will allow an operator to add, remove and modify targets on a 2D "grid world" map. We will not implement the actual user interface (UI); instead, we will concentrate on the logic and model behind the UI. We will implement a Target class and a Grid class that manages a collection of targets. The application should support the following actions.

In addition, the application should support unlimited undo's and redo's of the above actions. The Grid class should provide an interface to allow the UI to get a list of targets and query specific locations on the grid.

This example is quite small and simple, and you may feel very comfortable tackling this problem without TDD or unit-testing. To fully appreciate the benefits of TDD, you would have to work on a much larger, more complex system. But we don't have time in this short course to work through such a system. At times, you may feel like you could jump ahead several steps because the "right" implementation is obvious to you. But keep in mind, the purpose of TDD is not to slow you down when you see the "obvious" implementation; it is to allow you to slow down and take the development one step at a time when you need to, because the right implementation is not obvious. In other words, the granularity of your tests can be as fine or as coarse as you want---though making the unit test cases too coarse will nullify any of the benefits of TDD.

C2 App Demo

Here is a working demo of the application we will develop to give you a better understanding of what we will be doing. Again, we will not develop the UI, only the model behind the UI.

Empty

First Steps: Creating a Target class

We must implement the Target class. Before we can use Target objects we must be able to create them. So we will start by considering how Target objects should be instantiated. Since we are doing TDD, we do not immediately implement the Target class constructor method; instead we start by writing a test. To write the test we must decide on an interface for creating Target objects. A target should know it's position, so we will have to pass a Point object to the Target constructor. The Point object should take 2 parameters, an x and a y coordinate. We will also need a second Point object to use to compare with the position of the Target (we want to compare the points' x and y values, not their memory addresses.)

test.coffee
test 'Basic Target class', ->
    point1 = new target.Point 1, 2
    point2 = new target.Point 1, 2
    t = new target.Target point1
    ok t.position.equal point2 

So our test creates two Point objects and then uses the first one to create a Target object and finally assert that the target's position is equal to the second Point object. Notice how we have not implemented the solution yet; we have instead designed the interface to create Targets and Points and the interface to compare if 2 Points are equal. We also defined a module named target to serve as the name-space for the code we will be developing. Now we run the test, which fails because Point and Target classes have not been implemented yet. This is what we want. Always ensure a new test fails before you write the code to pass the test--this way we know the test works.

Now we can write the code to pass the test. This code is very straightforward. The required interfaces are already defined in the test.

code.coffee
window.target = {}

class target.Point
    constructor: (@x, @y) ->

    equal: (other) ->
        other.x == @x and other.y == @y


class target.Target
    constructor: (@position) ->

Running the test shows the test now passes. Great. Now let's extend the test to exercise all the basic functions of the Target class. A Target should be able to change its type (friendly, enemy, or neutral), change its name, and move to a new position. No real logic or computation is needed to implement these basic functions, so I feel comfortable lumping all this into one test. If any of these functions where more complex, I would break them out into separate tests.

test.coffee
test 'Basic Target class', ->
    Point = target.Point
    point1 = new Point 1, 2
    point2 = new Point 1, 2
    t1 = new target.Target point1
    t2 = new target.Target point2
    ok (t1.position.equal point2), 'equal position'
    equal t1.type, t2.type, 'equal_type'
    equal t1.name, t2.name, 'equal_name'
    equal t1.type, 'neutral', 'default_type'
    equal t1.name, 'default-name', 'default_name'
    t1.change_type 'friendly'
    equal t1.type, 'friendly', 'change_type'
    t1.change_name 'F-16'
    equal t1.name, 'F-16', 'change_name'
    t1.move(new Point 5, 3)
    ok t1.position.equal(new Point 5, 3), 'move'

Since we create several Point objects, I brought the Point class into the local name-space (line 2) so we don't have to continually prefix Point with the target module. Since Targets can change their types and names, they should have a default type and name when created. So we test that if we create two targets, t1 and t2, they have a type and a name already defined and equal to each other. Then we try changing the type and name and check if they were actually changed. Finally, we move the target and assert the target moved to the new position. We provided labels for each assertion (each call to ok or equal) so we could easily tell them apart in the test results. You can see the individual assertions by clicking on the test name "Basic Target class" in example1-test.html.

As expected, the test fails. Now it is time to implement the code to pass the test.

code.coffee
class target.Target
    constructor: (@position) ->
        @type = 'neutral'
        @name = 'default-name'

    change_type: (type) ->
        @type = type

    change_name: (name) ->
        @name = name

    move: (point) ->
        @position = point

Again, the code is very straightforward. We initialize the type and name instance variables in the Target constructor and provide 3 new methods that set the respective instance variable to a new value (these are known as "setter" methods.) Running the test shows that we have met our goal.

Adding the Undo Feature

Great, we have the basic structure of our Target class. Code this trivial does not need a special test like we have done; but in order to facilitate learning the TDD process, we started with this simple test. Now we will deal with slightly more complex code. Let's add the 'undo' feature to the class. We'll start by defining a test. We will need to create a target and call a few functions on the object to change its state. Then we'll call the undo function a few times; each time ensuring that the state is reverting to the state prior to the last action.

test.coffee
test 'Undo', ->
    Point = target.Point
    point1 = new Point 1, 2
    point2 = new Point 4, 3
    point3 = new Point 5, 6
    t = new target.Target point1
    t.move point2
    t.change_type 'enemy'
    t.change_name 'B-2'
    t.move point3
    equal t.type, 'enemy', 'Type changed to enemy'
    equal t.name, 'B-2', 'Name changed to B-2'
    ok t.position.equal(point3), 'moved to point3'
    t.undo()
    equal t.type, 'enemy', 'Type still enemy'
    equal t.name, 'B-2', 'Name still B-2'
    ok t.position.equal(point2), 'Moved back to point2'

Refactoring into a check_state() Function

I started writing this test but had to stop midway. There is to much duplicate code when testing the target's state. It requires 3 lines of code to check the position, type and name. We have already repeated this twice and will have to do it 2 more times for the other 2 calls to undo. Let's refactor the code for checking the target's state into a single function before moving on.

test.coffee
check_state = (target, point, type, name) -> 
    ok target.position.equal(point), 'check position'
    equal target.type, type, 'check type'
    equal target.name, name, 'check name'


test 'Undo', ->
    Point = target.Point
    point1 = new Point 1, 2
    point2 = new Point 4, 3
    point3 = new Point 5, 6
    t = new target.Target point1
    t.move point2
    t.change_type 'enemy'
    t.change_name 'B-2'
    t.move point3
    check_state t, point3, 'enemy', 'B-2' 
    t.undo()
    check_state t, point2, 'enemy', 'B-2' 
    t.undo()
    check_state t, point2, 'enemy', 'default-name' 
    t.undo()
    check_state t, point2, 'neutral', 'default-name'
    t.undo()
    check_state t, point1, 'neutral', 'default-name'

So we added a check_state function to handle ensuring the target's state is equal to some desired state and then completed writing the test. Running the test fails, so we are now ready to implement the undo feature in the production code.

We will need some way of saving a target's state each time a change is made. A stack data structure fits well here. Each call to change_type, change_name, or move should save the target's current state by pushing the target's state onto the stack, and each call to undo should pop the target's state from the stack and restore it. Fortunately, Javascript arrays also have a stack API (push and pop), so we can just create an array in the constructor and assign it to an instance variable named past_states.

code.coffee
class target.Target
    constructor: (@position) ->
        @type = 'neutral'
        @name = 'default-name'
        @past_states = []

    change_type: (type) ->
        @save_state()
        @type = type

    change_name: (name) ->
        @save_state()
        @name = name

    move: (point) ->
        @save_state()
        @position = point

    save_state: ->
        @past_states.push [@position, @type, @name]

    undo: ->
        [@position, @type, @name] = @past_states.pop()

We implement the undo feature as expected. Since we push the current state onto the stack in three different functions (change_type, change_name, and move), we add a save_state function (lines 19-20) to encapsulate this behavior and prevent duplication. Running the two tests again with the new code results in all tests passing. We have successfully implemented the basic undo feature. However, before we move on, we have some cleaning up to do.

Refactoring Common Setup Code

There is a lot of duplication in our test code. We must keep our tests clean---if they become difficult to maintain, we will not want to add new tests or fix broken tests, thus reducing the effectiveness of TDD and our unit tests in general.

test.coffee
module 'test_target', {
    setup: ->
        Point = target.Point
        @point1 = new Point 1, 2
        @point2 = new Point 1, 2
        @point3 = new Point 4, 3
        @point4 = new Point 5, 6
        @t1 = new target.Target @point1
        @t2 = new target.Target @point2
        @Point = Point
}


get_state = (target) ->
    [target.position, target.type, target.name]


are_states_equal = (state1, state2) ->
    state1[0].equal(state2[0]) and
        (state1[1] == state2[1]) and (state1[2] == state2[2])


are_targets_equal = (target1, target2) ->
    are_states_equal (get_state target1), (get_state target2)


test 'Basic Target class', ->
    ok (are_targets_equal @t1, @t2), 'default targets equal'
    equal @t1.type, 'neutral', 'default type'
    equal @t1.name, 'default-name', 'default name'
    @t1.change_type 'friendly'
    equal @t1.type, 'friendly', 'change_type'
    @t1.change_name 'F-16'
    equal @t1.name, 'F-16', 'change_name'
    @t1.move(new @Point 5, 3)
    ok @t1.position.equal(new @Point 5, 3), 'move'


check_state = (target, point, type, name, msg) -> 
    result = are_states_equal (get_state target), [point, type, name]
    ok result, msg


test 'Undo', ->
    @t1.move @point3
    @t1.change_type 'enemy'
    @t1.change_name 'B-2'
    @t1.move @point4
    check_state @t1, @point4, 'enemy', 'B-2', 'state before undos'
    @t1.undo()
    check_state @t1, @point3, 'enemy', 'B-2', 'undo last move'
    @t1.undo()
    check_state @t1, @point3, 'enemy', 'default-name', 'undo name' 
    @t1.undo()
    check_state @t1, @point3, 'neutral', 'default-name', 'undo type'
    @t1.undo()
    check_state @t1, @point2, 'neutral', 'default-name', 'undo move'

Here's our refactored test code. We create a test module called 'test_target' and place redundant test setup code inside its setup method (the setup method gets rerun before each test function in the module to ensure tests don't interfere with each other.) We also create get_state, are_state_equal, and are_targets_equal helper functions to make the code more readable and explicit. The check_state function is simplified and receives a label. The @ symbol is shorthand for "this.", so @name is really means this.name. The this symbol always refers to the enclosing environment. So in the setup method, we bound several variables to the module so we could reuse them in the test functions. That is why they are prefixed with the @ symbol in the setup and test functions. Re-run the tests to ensure everything still works and we didn't break the tests while refactoring.

The Redo Feature

Now we will incorporate the "redo" feature. To test redo() we must first create a target and perform some basic actions on it. Then we need to perform a couple of undo() operation so we have something to redo. Because these are the same steps as the Undo test, we move the common code into a perform_4_actions() function and an undo_2_actions() function. Our Undo and Redo tests call the perform_4_actions() and undo_2_actions() functions first. The Redo test then calls redo() twice; each time ensuring the target's state reverts to it's state prior to the corresponding undo.

test.coffee
perform_4_actions = (t1, point3, point4) ->
    t1.move point3
    t1.change_type 'enemy'
    t1.change_name 'B-2'
    t1.move point4
    check_state t1, point4, 'enemy', 'B-2', 'state before undos'


undo_2_actions = (t1, point3, point4) ->
    t1.undo()
    check_state t1, point3, 'enemy', 'B-2', 'undo last move'
    t1.undo()
    check_state t1, point3, 'enemy', 'default-name', 'undo name' 


test 'Undo', ->
    perform_4_actions(@t1, @point3, @point4)
    undo_2_actions(@t1, @point3, @point4)
    @t1.undo()
    check_state @t1, @point3, 'neutral', 'default-name', 'undo type'
    @t1.undo()
    check_state @t1, @point2, 'neutral', 'default-name', 'undo move'


test 'Redo', ->
    perform_4_actions(@t1, @point3, @point4)
    undo_2_actions(@t1, @point3, @point4)
    @t1.redo()
    check_state @t1, @point3, 'enemy', 'B-2', 'Redo change name'
    @t1.redo()
    check_state @t1, @point4, 'enemy', 'B-2', 'Redo move point4'

Running the test fails because redo has not yet been implemented. We will do that now. It looks like redo should work just like undo but in reverse. So we could use another stack, future_states, to capture states that have been "undone".

code.coffee
class target.Target
    constructor: (@position) ->
        @type = 'neutral'
        @name = 'default-name'
        @past_states = []
        @future_states = []

    change_type: (type) ->
        @save_state()
        @type = type

    change_name: (name) ->
        @save_state()
        @name = name

    move: (point) ->
        @save_state()
        @position = point

    save_state: ->
        @past_states.push [@position, @type, @name]

    undo: ->
        @future_states.push [@position, @type, @name]
        [@position, @type, @name] = @past_states.pop()

    redo: ->
        @past_states.push [@position, @type, @name]
        [@position, @type, @name] = @future_states.pop()
Line 6
The constructor initializes a new instance variable, future_states, which references a stack that holds the future states of the target object.
Line 24
Every call to undo() now saves the current state by pushing it onto the future_states stack prior to restoring the most recent previous state from the top of the past_states stack.
Lines 27-29
The redo() function performs the opposite actions of the undo() function. It pushes the current state onto the past_states stack and then restores the next "future" state.

Running the 3 tests indicates everything works. That's good. Before we move on, we should refactor to eliminate duplication. There are three issues we should address:

  1. Every time we push the current state, we construct an array with the current position, type and name. This is done in 3 places, so we should extract that into a method called get_current_state (lines 8-9). The save_state() and undo() methods can now make use of the get_current_state() method (lines 12 and 27).
  2. Now that we have two state stacks (past and future), the save_state() function is ambiguous. Which stack is it saving to? Let's rename that method so it is more clear. We'll call it store_past_state (line 11). All former calls to save_state() must be replaced with calls to store_past_state() (lines 15, 19, and 23).
  3. The redo() method duplicates the code in store_past_state(), so redo() should just call store_past_state() (line 31).
code.coffee
class target.Target
    constructor: (@position) ->
        @type = 'neutral'
        @name = 'default-name'
        @past_states = []
        @future_states = []

    get_current_state: ->
        [@position, @type, @name]

    store_past_state: ->
        @past_states.push @get_current_state()

    change_type: (type) ->
        @store_past_state()
        @type = type

    change_name: (name) ->
        @store_past_state()
        @name = name

    move: (point) ->
        @store_past_state()
        @position = point

    undo: ->
        @future_states.push @get_current_state()
        [@position, @type, @name] = @past_states.pop()

    redo: ->
        @store_past_state()
        [@position, @type, @name] = @future_states.pop()

After each change, I rerun the tests to ensure I didn't brake anything during the refactorings. If you re-test between small changes, it is easier to catch your mistakes where they happen which makes them much easier to fix.

Using Exceptions to Enforce Proper Use

All the tests are passing, but what happens if we call undo() when the past_states stack in empty? What if we call redo() when the future_states is empty---what happens then? The Target instance should throw a descriptive exception. We will have to define an 'exception' class we can use for this situation. We will start by writing a test that makes use of our new 'exception' class.

test.coffee
test 'StackException', ->
    exp_str1 = "Empty past_states stack during 'undo' " +
               "method from Target<name: default-name, " +
               "type: neutral, @ P(1, 2)>."
    exp_str2 = "Empty future_states stack during 'redo' " +
               "method from Target<name: default-name, " +
               "type: neutral, @ P(1, 2)>."
    act_str1 = (new target.StackException(@t1, 'past')).toString()
    act_str2 = (new target.StackException(@t2, 'future')).toString()
    equal act_str1, exp_str1
    equal act_str2, exp_str2

For this test, we define the strings we want the exception to display, and then create the exception, convert it to a string and compare the actual error message with the expected error message. This requires both our Point and Target classes to have toString methods (the javascript interpreter uses the toString method of a class to convert an instance of the class to a string.) We will add a toString method to the Point and Target classes and then implement the actual StackException class.

code.coffee
class target.Target
    ... other code ...

    toString: ->
        "Target<name: #{@name}, type: #{@type}, @ #{@position}>" 


class target.Point
    ... other code ...

    toString: ->
        "P(#{@x}, #{@y})"


class target.StackException
    constructor: (@target, @stack_name) ->
        @message = this.toString()

    toString: ->
        if @stack_name == 'past'
            method = 'undo'
        else
            method = 'redo'
        "Empty #{@stack_name}_states stack during '#{method}' " + 
            "method from #{@target}."

Our StackException test passes. Now we can focus on the real test. We want a test that ensures a Target instance throws a StackException when undo() is called with an empty past_states stack or redo() is called with an empty future_states stack. Let's add a couple of tests that do just that.

test.coffee
test 'Invalid undo; past_states is empty', ->
    t1 = @t1
    throws (-> t1.undo()), target.StackException


test 'Invalid redo; future_states is empty', ->
    t2 = @t2
    throws (-> t2.redo()), target.StackException

The two new tests fail when run. Now let's fix the undo and redo methods. The only time we want the StackException to be thrown is when either the past or future states stacks are empty during an undo or redo operation. So we can simply check if the respective stack is empty at th beginning of the respective methods and throw a StackException if true.

code.coffee
class target.Target
    ... other code ...

    undo: ->
        if @past_states.length == 0
            throw new target.StackException(this, 'past')
        @future_states.push @get_current_state()
        [@position, @type, @name] = @past_states.pop()

    redo: ->
        if @future_states.length == 0
            throw new target.StackException(this, 'future')
        @store_past_state()
        [@position, @type, @name] = @future_states.pop()

All tests now pass. But what if we call redo() after a new action is performed? The redo() method only makes sense immediately after a call to undo(). You should not be able to redo() after a new action; it doesn't make any sense. Let's write a test to catch that potential bug.

test.coffee
test 'Clear future states on new action', ->
    perform_4_actions(@t1, @point3, @point4)
    undo_2_actions(@t1, @point3, @point4)
    @t1.move @point4
    throws (-> @t1.redo()), target.StackException, 'Trying to ' + 
        'redo after a new action causes an empty stack exception'

All we need to do is clear the future_states stack during each action. The only actions are the change_type(), change_name(), and move() methods.

code.coffee
class target.Target
    ... other code ...

    change_type: (type) ->
        @store_past_state()
        @type = type
        @future_states = []

    change_name: (name) ->
        @store_past_state()
        @name = name
        @future_states = []

    move: (point) ->
        @store_past_state()
        @position = point
        @future_states = []

    ... other code ...

All tests now pass. But all three methods do the same thing. Let's refactor the common code into a set_attribute() method.

code.coffee
class target.Target
    ... other code ...

    change_type: (type) ->
        @set_attribute("type", type)

    change_name: (name) ->
        @set_attribute("name", name)

    move: (point) ->
        @set_attribute("position", point)

    set_attribute: (name, value) ->
        @store_past_state()
        this[name] = value
        @future_states = []

    ... other code ...

All 7 tests pass and we can be reasonably sure that if our future code has a bug that performs an invalid call to undo or redo, the target object will immediately throw a descriptive exception. So if the bug does find its way into our code, it will be quickly and explicitly caught, making it much easier to fix.

Figure 1: Class Diagram

Target class diagram

Figure 1 presents the class diagram of the code we have written so far. Through this TDD process, we have defined an application programming interface (API) to the Target class that other objects may use to create and manipulate instance objects of the Target class. The Target API is summarized in the table below.

Target API
Method Input parameters Return value
new Target Point Target
move Point null
change_typeString null
change_nameString null
undo null
redo null
toString String

Now that we have a working and tested Target class, let's move on to implementing a collection of targets. Continue to Example 1.2.

Here is the full Target class code listing and the Target test code for your convenience.

Lyall Jonathan Di Trapani 28 Jan 2013