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

In Example 1.1, we implemented the Target class using TDD with automated unit tests. However, our application needs to manage and manipulate several target objects. Therefore, we need some sort of collection class for the targets. Since we are working in Cartesian coordinates, we will create a Grid class to hold our target objects.

Creating the Grid class: A Collection of Targets

The Grid class should be able to place, remove, and move targets. We should be able to retrieve targets from a grid object individually and all together as a list. Retrieving individual targets will allow the application to change the type or name of a single target. Retrieving all the targets at once in a list will allow the user interface to draw the targets on the screen. Finally, the Grid class should support undo and redo operations. It does not make sense for the Grid class to support changing the type or name of a target since those operations have nothing to do with a grid of targets.

We start with a test. We pick an arbitrary point, say (1, 2), and call it p1. Our test should do the following steps:

  1. Create a grid object.
  2. Check that no target is at point p1.
  3. Place a target at point p1.
  4. Check that the target exists at point p1.
  5. Remove the target.
  6. Ensure no target exists at point p1.
test.coffee
module 'test_grid'


test 'Place and remove targets', ->
    p1 = new target.Point 1, 2 
    grid = new target.Grid DummyTarget, 5, 5
    equal grid.has_target(p1),
          false,
          'Initially no target at point (1, 2)'
    grid.place p1
    equal grid.has_target(p1),
          true,
          'Target placed at point (1, 2)'
    t1 = grid.get_target(p1)
    grid.remove t1
    equal grid.has_target(p1),
          false,
          'Target removed at point (1, 2)'

We have created a new module called test_grid (line 1) to keep our Target class tests and Grid class tests separate. In this test, we have also declared that the Grid class has the following methods: has_target(), place(), get_target(), and remove(). Our test gives an example of how each of these methods are used. We have also defined the signature for the Grid constructor method. It takes a class as its first argument. Let's discuss the motive for this decision.

Dependency Injection

Good unit tests minimize dependencies. The fewer dependencies, the better the fault isolation. In other words, if a unit test isolates the unit of code under test from its dependencies, and the test fails, we can be sure the offending code is the unit under test and not one of its (potentially many) dependencies. The Grid class has a logical dependency on the Target class since a grid is a collection of targets. To remove this dependency, we have designed the Grid class, through the process of writing the above test, to take a class that represents a Target class as a constructor parameter. This way, during testing, we can use our own "stub" or "mock" DummyTarget class instead of using the real Target class. This practice is also known as "dependency injection" because it allows us to "inject" different dependencies when running tests versus when running the actual application. We could also try to remove the dependency on the Point class, but a Point is so simple, it would be a waste of time. Being aware of the dependencies of the unit under test and choosing which are worth removing via stub and mock objects is key to writing quality unit tests. In this specific situation, since the Target class is already tested, is relatively simple, and does not have any large dependencies, it is not necessary to use a mock target class. However, for the sake of meeting our learning objectives, we will apply dependency injection so we can have a clear idea how it is accomplished.

The test requires us to implement a DummyTarget class. Let's do that now.

test.coffee
class DummyTarget
    constructor: (@position) ->

The only thing our DummyTarget needs to do is take a Point object as a parameter, so it is very simple. Notice that we implemented the DummyTarget in the test.coffee file. It is test code not production code, so it goes in the test file. The test fails when run. Now let's get to work on the actual Grid class.

code.coffee
class target.Grid
    constructor: (@target_class, @width, @height) ->
        @map = {}

    has_target: (point) ->
        @get_target(point) != undefined

    get_target: (point) ->
        @map[point.toString()]

    place: (point) ->
        @map[point.toString()] = new @target_class point

    remove: (target) ->
        delete @map[target.position.toString()]

Favor Readability and Maintainability over Performance

We could have used a 2 dimensional array to implement the Grid which may yield better performance, but I decided to go with a map because it is easier to write. When faced with implementation choices, it is often better to choose the more readable and maintainable implementation over a high-performance, but less readable and maintainable implementation. Only performance tune code that is know to be a bottleneck through explicit measurement with a profiling tool. Remember, you have to maintain 100% of your source code, but you only need to performance tune a small percentage (usually no more than 10%, but sometimes none at all.) So it pays to go with the more maintainable solution by default and only performance tune the bottlenecks when absolutely necessary.

The map object simply maps string representations of Point objects to targets. If no target exists at the point, it is undefined. All tests now pass.

Moving Targets on the Grid

Let's deal with the move feature next. First we'll write a test. The test should take the following steps.

  1. Create a grid.
  2. Place a target at point p1.
  3. Move the target on the grid from p1 to p2.
  4. Check that target is no longer at p1.
  5. Check that a target is at p2.
  6. Ensure the target at p2 is the same target that was at p1.
  7. Check that the target's position attribute changed to p2.
test.coffee
test 'Move target on grid', ->
    p1 = new target.Point 1, 2 
    p2 = new target.Point 4, 3 
    grid = new target.Grid DummyTarget, 5, 5
    grid.place p1
    t1 = grid.get_target(p1)
    grid.move(t1, p2)
    ok (not grid.has_target(p1)), 'target no longer at p1'
    ok grid.has_target(p2), 'target now at p2'
    t2 = grid.get_target(p2)
    ok are_targets_equal(t1, t2), 'target at p2 is t1'
    ok t1.position.equal(p2),
       'target\'s position attribute was changed to p2'

Refactoring Common Setup Code

We have some duplication in the setup code of our Grid class tests. Let's refactor the duplication into a common setup method for the module.

test.coffee
module 'test_grid', {
    setup: -> 
        Point = target.Point
        @p1 = new Point 1, 2 
        @p2 = new Point 4, 3 
        @p3 = new Point 3, 5
        @grid = new target.Grid DummyTarget, 5, 5
        @grid.place @p1
        @grid.place @p3
}


test 'Place and remove targets', ->
    grid = @grid
    p2 = @p2
    equal grid.has_target(p2),
          false,
          'Initially no target at point p2'
    grid.place p2
    equal grid.has_target(p2),
          true,
          'Target placed at point p2'
    t1 = grid.get_target(p2)
    grid.remove t1
    equal grid.has_target(p2),
          false,
          'Target removed at point p2'


test 'Move target on grid', ->
    p1 = @p1
    p2 = @p2
    grid = @grid
    t1 = grid.get_target(p1)
    grid.move(t1, p2)
    ok (not grid.has_target p1), 'target no longer at p1'
    ok grid.has_target(p2), 'target now at p2'
    t2 = grid.get_target p2
    ok are_targets_equal(t1, t2), 'target at p2 is t1'
    ok t1.position.equal(p2),
       'target\'s position attribute was changed to p2'

Our test_grid module now has a setup method which runs before each test, providing a common base for each test to work from. We will need this setup code for future tests as well, so it was time well spent. We had to fix the two existing tests to take advantage of the new setup code. Running the new test fails, so we are ready for the grid.move() implementation.

code.coffee
class target.Grid
    ... other code ...

    move: (target, point) ->
        @remove(target)
        target.move(point)
        @map[point.toString()] = target

This code still fails because we call move on the target and our DummyTarget class does not (yet) define a move method. Let's fix that.

test.coffee
class DummyTarget
    constructor: (@position) ->

    move: (point) ->
        @position = point

Great, now our DummyTarget class (in test.coffee) has a simple move method. All our tests are now passing.

Testing Test Code

We need a method that returns all the targets in the grid at once. We'll call it get_target_list and it will return a list of targets. Let's write a test for it. The test should call get_target_list() when the grid has 2 targets and 3 targets, each time checking that the list of targets returned is correct.

test.coffee
test 'Get list of targets', ->
    grid = @grid
    t1 = grid.get_target(@p1)
    t3 = grid.get_target(@p3)
    ok are_target_lists_equal(grid.get_target_list(), [t1, t3]),
       'List with 2 targets'
    grid.place(@p2)
    t2 = grid.get_target(@p2)
    ok are_target_lists_equal(grid.get_target_list(), [t1, t3, t2]),
       'List with 3 targets'

Our setup method allows us to leverage the pre-built grid, p1, p2 and p3 objects. This test is simple, except it calls an are_target_lists_equal() function. We will have to define an are_target_lists_equal() helper function in the test code. It should compare two lists of targets and return true if both lists contain the same targets, regardless of order. Let's write a test for this first, so we have a well defined behavior. The test will give us confidence that we implemented the function properly. We don't want bugs in our test code!

test.coffee
test 'are_target_lists_equal', ->
    t1 = new DummyTarget @p3
    t2 = new DummyTarget @p2
    t3 = new DummyTarget @p1
    ok are_target_lists_equal [], []
    ok not (are_target_lists_equal [], [t1])
    ok are_target_lists_equal [t1], [t1]
    ok not (are_target_lists_equal [t2, t3, t1], [t2, t3])
    ok are_target_lists_equal [t1, t2, t3], [t2, t3, t1]

The above test can verify the are_target_lists_equal() helper function. At this point, we have two failing tests ("Get list of targets" and "are_targets_lists_equal"). Let's implement the are_target_lists_equal() function in the test code.

test.coffee
is_target_in_list = (target, list) ->
    bools = (are_targets_equal(target, entry) for entry in list)
    true in bools


are_target_lists_equal = (target_list1, target_list2) ->
    same_length = target_list1.length == target_list2.length
    bools = []
    for target in target_list1
        bools.push(is_target_in_list(target, target_list2))
    same_length and (not (false in bools))

Now the test passes and we can be confident in our are_target_lists_equal() helper function. We can finally implement the get_target_list() function in the grid class.

code.coffee
class target.Grid
    ... other code ...

    get_target_list: ->
        target_list = []
        for x in [1..@width]
            for y in [1..@height]
                point = new target.Point x, y
                if @has_target(point)
                    target_list.push @get_target(point)
        target_list

Running our test suite shows all tests passing.

Undo and Redo the Place Method

No let's work on the undo and redo features. We will start by just focusing on the place() method. Let's write a test to undo a redo a place() action. These are the steps the test should take.

  1. Place target t1 at point p1 (already in setup code)
  2. Place target t3 at point p3 (already in setup code)
  3. Undo place at point p3
  4. Check that no target exists at point p3
  5. Undo place at point p1
  6. Check that no target exists at point p1
  7. Redo place at point p1
  8. Check that a target exists at point p1
  9. Redo place at point p3
  10. Check that a target exists at point p3
test.coffee
test 'Undo and redo place', ->
    grid = @grid
    grid.undo()
    ok (not grid.has_target(@p3)), 'Place at p3 undone'
    grid.undo()
    ok (not grid.has_target(@p1)), 'Place at p1 undone'
    grid.redo()
    ok grid.has_target(@p1), 'Place at p1 redone'
    grid.redo()
    ok grid.has_target(@p3), 'Place at p3 redone'

The test fails. Let's implement the production code. With the Target class, we just save the state of the target onto past and futures state stacks. Saving the state of the entire grid will likely be cumbersome. Instead, let's use the concept of actions. We will have two stacks; a past_actions stack and a future_actions stack. We need to define what an action is.

code.coffee
class Action
    constructor: (@name, @target, @position) ->

So an action is just a record that stores the action's name (the name of the method), the target involved with the action, and the position involved with the action. Notice that the Action class is not in the "target" module because it is not prefixed with "target.". Since we don't need to create Action objects in our test code, there is no point in making the Action class available to outside modules. Now we can work on the Grid class.

code.coffee
class target.Grid
    constructor: (@target_class, @width, @height) ->
        @map = {}
        @past_actions = []
        @future_actions = []

    ... other code ...

    place: (point) ->
        new_target = new @target_class point
        action = new Action 'place', new_target, point
        @past_actions.push action
        @map[point.toString()] = new_target

    undo_place: (action) ->
        @private_remove(action.position)

    redo_place: (action) ->
        @map[action.position.toString()] = action.target
        
    remove: (target) ->
        @private_remove(target.position)

    private_remove: (point) ->
        delete @map[point.toString()]

    undo: ->
        action = @past_actions.pop()
        @future_actions.push(action)
        this["undo_#{action.name}"](action)

    redo: ->
        action = @future_actions.pop()
        @past_actions.push(action)
        this["redo_#{action.name}"](action)
Lines 4-5
We add the two stacks as instance variables in the constructor.
Lines 9-13
The place() method needs to create an Action object that records the data involved in the 'place' action and then save the action on the past_actions stack.
Lines 27-30
The undo() method pops the most recent action off of the past_actions stack and saves it onto the future_actions stack. It then calls the specific undo_* method according to the name of the action.
Lines 32-35
The redo() method does the opposite. It pops the most recent action off of the future_actions stack and pushes it onto the past_actions stack. It then calls the specific redo_* method according to the name of the action.
Lines 15-19
We define undo_place() and redo_place() methods that know how to undo and redo a given 'place' action.
Lines 24-25
We also added a private_remove method since both the remove() and the undo_place() methods contain the same code.

All tests now pass and we can move on.

Revisiting the move() Method

What if we try to move a target to a location on the grid that contains another target? This scenario was not dealt with previously. Let's take care of it now. Notice how the TDD process allows us to revisit old code and easily add new test cases and production code to capture functionality we may have missed the first time.

test.coffee
get_name = (grid, point) ->
    grid.get_target(point).name


test 'Move target to position occupied by another target', ->
    grid = @grid
    equal get_name(grid, @p1), 'A', 'target A at p1'
    equal get_name(grid, @p3), 'C', 'target C at p3'
    grid.move(@t1, @p3)
    equal get_name(grid, @p1), 'C', 'target C now at p1'
    equal get_name(grid, @p3), 'A', 'target A now at p3'

The test requires targets to have a name attribute, but our DummyTarget class does not have a name attribute. We'll have to add that to the DummyTarget class.

test.coffee
class DummyTarget
    constructor: (@position) ->
        @name = 'default-name'

    change_name: (name) ->
        @name = name

    move: (point) ->
        @position = point

Furthermore, our setup method should change the name of the 2 targets placed in the grid to 'A' and 'C'.

test.coffee
module 'test_grid', {
    setup: -> 
        Point = target.Point
        @p1 = new Point 1, 2 
        @p2 = new Point 4, 3 
        @p3 = new Point 3, 5
        @grid = new target.Grid DummyTarget, 5, 5
        @grid.place @p1
        @grid.place @p3
        @t1 = @grid.get_target(@p1)
        @t1.change_name('A')
        @t3 = @grid.get_target(@p3)
        @t3.change_name('C')
}

Running the new test fails. We are ready to update the move() method in the Grid class. The move() method should check if another target exists at the position we are moving the target to and take appropriate action.

code.coffee
class target.Grid
    move: (target, to_point) ->
        from_point = target.position
        if @has_target(to_point)
            other_target = @get_target(to_point)
            other_target.move(from_point)
            @map[from_point.toString()] = other_target
        else
            @private_remove(from_point)
        target.move(to_point)
        @map[to_point.toString()] = target

The new code will swap the two target's positions instead of just dropping the other target as it previously did. All tests now pass. However, before we move on, let's refactor. We have several "@map[point.toString()] = target" lines in our Grid class. Let's move that into a method called put() and call put() everywhere instead.

code.coffee
class target.Grid
    ... other code ...

    put: (target, point) ->
        @map[point.toString()] = target

    place: (point) ->
        new_target = new @target_class point
        action = new Action 'place', new_target, point
        @past_actions.push action
        @put(new_target, point)

    undo_place: (action) ->
        @private_remove(action.position)

    redo_place: (action) ->
        @put(action.target, action.position)
        
    remove: (target) ->
        @private_remove(target.position)

    private_remove: (point) ->
        delete @map[point.toString()]

    move: (target, to_point) ->
        from_point = target.position
        if @has_target(to_point)
            other_target = @get_target(to_point)
            other_target.move(from_point)
            @put(other_target, from_point)
        else
            @private_remove(from_point)
        target.move(to_point)
        @put(target, to_point)

    ... other code ...

The put() method is defined on lines 4-5. Lines 11, 17, 30, and 34 now call put() instead. The changes make the code more readable. All our tests are still passing, so we can move forward.

Undo and Redo the move() Methods

Now let's continue implementing undo and redo. We will add undo and redo for the move() method. As usual, we start with the test. The test will do 2 moves, undo both moves, and finally redo the 2 moves. At each step, it checks that the positions of the targets are correct.

test.coffee
check_positions = (grid, point_empty, point_a, point_c, msg) ->
    empty = not grid.has_target(point_empty)
    target_a = get_name(grid, point_a) == 'A'
    target_c = get_name(grid, point_c) == 'C'
    ok (empty and target_a and target_c), msg


test 'Undo and redo move', ->
    p1 = @p1
    p2 = @p2
    p3 = @p3
    grid = @grid
    check_positions(grid, p2, p1, p3, 'before moves')
    grid.move(@t1, p2)   # t1 goes from p1 to p2
    grid.move(@t1, p3)   # t1 at p3 and t3 at p2
    check_positions(grid, p1, p3, p2, 'after moves')
    grid.undo()
    check_positions(grid, p1, p2, p3, 'after 1 undo')
    grid.undo()
    check_positions(grid, p2, p1, p3, 'after 2 undos')
    grid.redo()
    check_positions(grid, p1, p2, p3, 'after 1 redo')
    grid.redo()
    check_positions(grid, p1, p3, p2, 'after 2 redos')

While writing the test, I had a lot of duplicate code to check the positions of the targets for each step. So I extracted the common code into a check_positions() function. The test fails, so we are ready to implement the production code.

code.coffee
class Action
    constructor: (@name, @target, @point, 
                  @to_point=null, @other_target=null) ->


class target.Grid
    ... other code ...

    move: (target, to_point) ->
        from_point = target.position
        other_target = null
        if @has_target(to_point)
            other_target = @get_target(to_point)
            other_target.move(from_point)
        target.move(to_point)
        action = new Action('move', target, from_point, 
                            to_point, other_target)
        @past_actions.push action
        @private_move(target, from_point, to_point)

    private_move: (target, from_point, to_point) ->
        if @has_target(to_point)
            other_target = @get_target(to_point)
            @put(other_target, from_point)
        else
            @private_remove(from_point)
        @put(target, to_point)

    undo_move: (action) ->
        target = action.target
        @private_move(target, action.to_point, action.point)
        target.undo()
        other_target = action.other_target
        if other_target != null
            other_target.undo()

    redo_move: (action) ->
        target = action.target
        @private_move(target, action.point, action.to_point)
        target.redo()
        other_target = action.other_target
        if other_target != null
            other_target.redo()

    ... other code ...
Lines 1-3
A move action is more complex than a place action. It requires not only to keep track of the target and origin position, but also the destination position (to_point). If there exists a target object at the destination position, that target (other_target) must be stored in the Action object as well. So we add 2 optional instance variables to the Action class---to_point and other_target.
Lines 10-19
We change the move() method so it creates an Action object and stores it to the past_actions stack.
Lines 29-43
We add undo_move() and redo_move() methods which can undo and redo a given 'move' Action.
Lines 21-27
The move(), undo_move(), and redo_move() share common functionality. The common functionality is factored into the private_move() method.

The test still fails because the DummyTarget class does not yet define undo() and redo() methods. Let's add those methods to the DummyTarget class.

test.coffee
class DummyTarget
    ... more code ...

    undo: ->

    redo: ->

The DummyTarget class now has undo() and redo() methods with empty bodies. All tests pass.

Undo and Redo the remove() Method

This test is similar to the "Undo and redo move" test. Do 2 remove(), then 2 undo(), followed by 2 redo(). Check that the position of the targets on the grid are correct at each step.

test.coffee
check_targets = (grid, t1, t3, msg) ->
    t1_correct = grid.has_target(check_targets.p1) == t1
    t2_correct = grid.has_target(check_targets.p3) == t3
    ok (t1_correct and t2_correct), msg


test 'Undo and redo remove', ->
    grid = @grid
    check_targets.p1 = @p1
    check_targets.p3 = @p3
    check_targets(grid, true, true, 'before removes')
    grid.remove(@t1)
    grid.remove(@t3)
    check_targets(grid, false, false, 'after removes')
    grid.undo()
    check_targets(grid, false, true, 'after 1 undo')
    grid.undo()
    check_targets(grid, true, true, 'after 2 undos')
    grid.redo()
    check_targets(grid, false, true, 'after 1 redo')
    grid.redo()
    check_targets(grid, false, false, 'after 2 redos')

The test fails when run. As before, the common code is refactored into the check_targets() function. We can now implement the undo and redo functionality for the remove method.

code.coffee
class target.Grid
    ... other code ...

    remove: (target) ->
        point = target.position
        action = new Action 'remove', target, point
        @past_actions.push action
        @private_remove(target.position)

    private_remove: (point) ->
        delete @map[point.toString()]

    undo_remove: (action) ->
        @put(action.target, action.point)

    redo_remove: (action) ->
        @private_remove(action.point)

    ... other code ...

We follow the same pattern we established with the place() and move() methods. We add code to the remove() method to create an Action object and store it in the past_actions stack. Then we define the undo_remove and redo_remove methods. All tests now pass.

Revisiting the Undo and Redo Move Test

The move action is special because a Target object keeps track of its own position. So moving effects the Grid object and all Target objects involved in the move action. This issue arises in three methods: move(), undo_move(), and redo_move(). These methods should call the appropriate methods on the Target objects involved in the action. The appropriate Target method for each Grid method are shown below.

Grid method should call Target method.
Grid methodTarget method
move()move()
undo_move()undo()
redo_move()redo()

Our test does not check that these methods are actually called on all Targets involved in the move action. Let's modify our "Undo and redo move" test to incorporate these checks.

test.coffee
test_and_reset = (target, attribute) ->
    ok target[attribute] is true,
       "Target #{target.name} '#{attribute}' is true"
    target[attribute] = false


test 'Undo and redo move', ->
    p1 = @p1
    p2 = @p2
    p3 = @p3
    grid = @grid
    check_positions(grid, p2, p1, p3, 'before moves')
    grid.move(@t1, p2)   # t1 goes from p1 to p2
    test_and_reset(@t1, 'moved')
    grid.move(@t1, p3)   # t1 at p3 and t3 at p2
    check_positions(grid, p1, p3, p2, 'after moves')
    test_and_reset(@t1, 'moved')
    test_and_reset(@t3, 'moved')
    grid.undo()
    check_positions(grid, p1, p2, p3, 'after 1 undo')
    test_and_reset(@t1, 'undone')
    test_and_reset(@t3, 'undone')
    grid.undo()
    check_positions(grid, p2, p1, p3, 'after 2 undos')
    test_and_reset(@t1, 'undone')
    grid.redo()
    check_positions(grid, p1, p2, p3, 'after 1 redo')
    test_and_reset(@t1, 'redone')
    grid.redo()
    check_positions(grid, p1, p3, p2, 'after 2 redos')
    test_and_reset(@t1, 'redone')
    test_and_reset(@t3, 'redone')

The key to this test is the test_and_reset() function. It takes as parameters a target and an attribute. The function asserts that the attribute is set to true then resets the attribute to false. This function is called on the affected targets after each call to grid.move(), grid.undo(), and grid.redo() to ensure the required methods were called on the targets involved in the actions. By creating this test, we have designed a mechanism to allow us to verify that a particular method was called on a Target object without using the real Target object. Of course, this mechanism does not exist in our DummyTarget class yet, so let's modify the DummyTarget class to behave accordingly.

Extending a Mock Object

test.coffee
class DummyTarget
    constructor: (@position) ->
        @name = 'default-name'
        @moved = false;
        @undone = false;
        @redone = false;

    change_name: (name) ->
        @name = name

    move: (point) ->
        @position = point
        @moved = true

    undo: ->
        @undone = true

    redo: ->
        @redone = true

The changes to the DummyTarget class are trivial. We just add 'moved', 'undone', and 'redone' instance variables to the constructor. Each method sets the corresponding instance variable to true. This allows DummyTarget objects to keep a record of what methods were called during a test. Now we can use the test_and_reset() function to verify that the right functions were called on the DummyTarget objects. Classes that implement this type of "mock" functionality are usually referred to as mock classes, because on the surface, they appear to be the real thing, but under the hood, they actually only record what has been done to them to aid in testing. The use of this mock object has allowed us to avoid using the real Target object. This is beneficial as our test cases for the Grid class are independent of the Target class and therefore, more reliable. In general, the judicial use of mock objects can enable your unit tests to better isolate faults as well as test code that has dependencies that have not been implemented yet.

Running the test actually passes because we wrote code to call the correct methods on the first try. How do we deal with this? Change the production code to make the test fail. Let's delete the line target.move() from the grid.move() method. Running the test now results in a failure. Putting the line back in makes the test pass. Repeat that process for the call to other_target.move() in grid.move(), target.undo() and other_target.undo() in grid.undo(), and finally target.redo() and other_target.redo() in grid.redo(). Each time, the test fails with the line removed and passes when the line is reinserted. We can have confidence that the test is working properly, and therefore, our code is working properly.

Refactoring Exceptions

Like the Target class, the Grid class is also susceptible to bugs where undo or redo is called and the respective stack is empty. We can use the same pattern we used in the Target class to solve the problem in the Grid class. We made use of a StackException class to deal with this issue for the Target class. But we can't use it "as is" for the Grid class, because the guts of the StackException class were designed only for the Target class in mind. We should rename the StackException class to something more meaningful and then create a separate Exception class for the Grid class. We'll call the Target's version TargetStackException. We should start by modifying the tests that make use of the old StackException before making changes to the production code.

test.coffee
test 'TargetStackException', ->
    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)>."
    ex1 = new target.TargetStackException(@t1, 'past')
    ex2 = new target.TargetStackException(@t2, 'future')
    equal ex1.toString(), exp_str1
    equal ex2.toString(), exp_str2


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


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

Three tests make use of the StackException. We simply rename each occurrence of StackException to TargetStackException (lines 1, 6, 7, 14 and 19). The tests now fail. Let's fix the production code next.

code.coffee
class StackExceptionBase
    constructor: (@name_head, @name_tail, @target=null) ->
    
    toString: ->
        method = if (@name_head == 'past')
                    'undo'
                 else
                    'redo'
        origin = if (@target is null)
                    'grid'
                 else
                     @target.toString()
        "Empty #{@name_head}_#{@name_tail} stack during " + 
            "'#{method}' method from #{origin}."


class target.TargetStackException
    constructor: (target, stack_name) ->
        @base = new StackExceptionBase stack_name, 'states', target
        @message = @base.toString()

    toString: ->
        @base.toString()

We could simply rename the class to TargetStackException to make the tests pass. But since we know we want two versions of the Exception (a TargetStackException and a GridStackException), we will go ahead and factor the common code into a StackExceptionBase class and then create a TargetStackException class that configures a StackExceptionBase object and delegates behavior to it. Since this class is so simple, I feel comfortable doing this in one step. But if the class was more complex, I would hold off on writing the StackExceptionBase class until both the TargetStackException and GridStackException classes have been written and tested. Then I would refactor the common code into the StackExceptionBase class from the two concrete StackException classes. It is much easier to refactor code from complete and tested classes than it is to factor directly from one's imagination.

With these changes, the "TargetStackException" test passes, but the "Invalid undo" and "Invalid redo" tests both fail. We still need to update the Target.undo() and Target.redo() methods in the production code.

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

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

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

All we needed was to update the exception names from StackException to TargetStackException (lines 6 and 12). Now all our tests pass again and we are ready to deal with the exceptional behavior in the Grid.undo() and Grid.redo() methods.

Exceptional Behavior in undo() and redo() Methods

First, we need a GridStackException class. Let's write a test for the class.

test.coffee
test 'GridStackException', ->
    exp_str1 = "Empty past_actions stack during 'undo' method" + 
        " from grid."
    exp_str2 = "Empty future_actions stack during 'redo' method" + 
        " from grid."
    act_str1 = new target.GridStackException('past').toString()
    act_str2 = new target.GridStackException('future').toString()
    equal act_str1, exp_str1
    equal act_str2, exp_str2

This is just like the "TargetStackException" test, only modified for the GridStackException class. Running the test fails because the GridStackException class does not exist. We will create it now.

code.coffee
class target.GridStackException
    constructor: (stack_name) ->
        @base = new StackExceptionBase stack_name, 'actions'
        @message = @base.toString()

    toString: ->
        @base.toString()

The GridStackException class is just like the TargetStackException, it configures a new StackExceptionBase object and then delegates behavior to it. All our tests are passing again. We can finally write tests for invalid undo and invalid redo.

test.coffee
test 'Invalid undo, past_actions stack is empty', ->
    grid = new target.Grid DummyTarget, 5, 5
    throws (-> grid.undo()), target.GridStackException


test 'Invalid redo, future_actions stack is empty', ->
    grid = new target.Grid DummyTarget, 5, 5
    throws (-> grid.redo()), target.GridStackException

Both tests fail because the Grid class is not checking for an empty stack condition before running the undo() and redo() code. We must add the appropriate checks to the undo() and redo() methods and throw an exception if needed.

code.coffee
class target.Grid
    ... other code ...

    undo: ->
        if @past_actions.length == 0
            throw new target.GridStackException('past')
        action = @past_actions.pop()
        @future_actions.push(action)
        this["undo_#{action.name}"](action)

    redo: ->
        if @future_actions.length == 0
            throw new target.GridStackException('future')
        action = @future_actions.pop()
        @past_actions.push(action)
        this["redo_#{action.name}"](action)

All tests now pass.

Clear Future Actions Stack on New Action

We have a similar problem as with the Target class; a grid should not be able to redo() after a new action. A grid should only be able to redo() after an undo() or a redo(). So let's add a test for that.

test.coffee
test 'Clear future actions stack on new action', ->
    @grid.move(@t1, @p2)
    @grid.undo()
    @grid.place(@p2)
    throws (-> @grid.redo()), target.GridStackException

We simply move target t1 to point p2, undo the move, perform a new action (placing a new target at point p2) and then check that a GridStackException is thrown when we try to redo the move. The test fails because no exception is thrown. Let's modify the production code to clear the future_actions stack after each new action.

code.coffee
class target.Grid
    ... other code ...

    clear_future_actions: ->
        @future_actions = []

    place: (point) ->
        @clear_future_actions()
        new_target = new @target_class point
        action = new Action 'place', new_target, point
        @past_actions.push action
        @put(new_target, point)

    remove: (target) ->
        @clear_future_actions()
        point = target.position
        action = new Action 'remove', target, point
        @past_actions.push action
        @private_remove(target.position)

    move: (target, to_point) ->
        @clear_future_actions()
        from_point = target.position
        other_target = null
        if @has_target(to_point)
            other_target = @get_target(to_point)
            other_target.move(from_point)
        target.move(to_point)
        action = new Action('move', target, from_point, 
                            to_point, other_target)
        @past_actions.push action
        @private_move(target, from_point, to_point)

    ...other code ...

We define a new function, clear_future_actions(), and call it at the start of the three methods that implement actions: place(), remove(), and move(). Now all our tests pass.

Return the Current Position from undo() and redo() Methods

While implementing the user interface (UI), I realized the UI needed to know the current position after a grid.undo() or grid.redo() operation so the UI could properly update the display to reflect the new state of the grid. Let's start by writing a test to check for this requirement before we alter the production code. The test should call the three actions of interest (place(), move(), and remove()) on a grid object. Then the test should call grid.undo() 3 times and grid.redo() 3 times, each time checking the point returned is correct.

test.coffee
check_point = (operation, expected_point, label) ->
    actual_point = check_point.grid[operation]()
    ok actual_point.equal(expected_point), "#{operation}: #{label}"


test 'Undo and redo return current position', ->
    @grid.move(@t1, @p2)
    @grid.remove(@t1)
    check_point.grid = @grid
    check_point 'undo', @p2, 'remove target t1'
    check_point 'undo', @p1, 'move target t1 from p1 to p2'
    check_point 'undo', @p3, 'place target at p3'
    check_point 'redo', @p3, 'place target at p3'
    check_point 'redo', @p2, 'move target t1 from p1 to p2'
    check_point 'redo', @p2, 'remove target t1'

The check_point() function simply calls grid.undo() or grid.redo() according to the operation parameter and then checks that the actual point returned (actual_point) equals the expected point (expected_point). The new test fails, so let's fix the production code.

code.coffee
class target.Grid
    ... other code ...

    undo: ->
        if @past_actions.length == 0
            throw new target.GridStackException('past')
        action = @past_actions.pop()
        @future_actions.push(action)
        this["undo_#{action.name}"](action)
        action.point

    redo: ->
        if @future_actions.length == 0
            throw new target.GridStackException('future')
        action = @future_actions.pop()
        @past_actions.push(action)
        this["redo_#{action.name}"](action)
        if action.name == 'move'
            action.to_point
        else
            action.point

This is an easy fix. Since undoing any of the 3 actions of interest result in action.point being the current position, the Grid.undo() method can simply return action.point (line 10). For the redo() method, we must consider redoing a move method separately, since the resulting position will be the action.to_point and not the action.point as it would be for the remove and place actions (lines 18-21). With the changes, all tests now pass. The complete class diagram is shown in Figure 1.

Figure 1: Class Diagram

Grid class diagram

Here is the Grid application programming interface (API) we have created through this process.

Grid API
Method Input parameters Return value
new Grid class X int X int Grid
has_target Point boolean
get_target Point Target
place Point null
remove Target null
move Target X Pointnull
get_target_list List of Targets
undo Point
redo Point

You can see all the tests run in qUnit here: test.html. Here is the full Grid class code listing and the Grid test code for your convenience. Also, here are the original code.coffee and test.coffee source files. The UI code is in app.coffee.

You should now be ready to tackle Exercise 1.

Lyall Jonathan Di Trapani 28 Jan 2013