Test-driven Development As If You Meant It Reviewed—'Normal' TDD
Published:
This article is part 5 in a 5-part series.
- Part 1: Test-driven Development As If You Meant It Reviewed—Introduction
- Part 2: Test-driven Development As If You Meant It Reviewed—The Problem
- Part 3: Test-driven Development As If You Meant It Reviewed—First Steps
- Part 4: Test-driven Development As If You Meant It Reviewed—Interesting Moments
- Part 5: This article
This article was written with and is also published by Mark Withall.
We’ve had a thorough look at TDDAIYMI over the last few parts of this series. Now we’re going to look at how the task might have been completed using ‘normal’ TDD.
Being British, we’re using a top-down approach that starts with the controller and works out to the model and view implementations; rather than the more traditional bottom up approach, which is more akin to TDDAIYMI.
We’ll start off in this part by looking, step-by-step, at each of the initial commits of the RED-GREEN-REFACTOR cycle.
The Starting Commits
As with all TDD, we start off by writing a failing test. We’re starting with the controller, as that seems to be the most logical place to start; it’ll tell us what we need from the model and from the view.
Our first test is that the controller calls the is_legal()
check on our model. However, we have a failing test at the point we try and construct the controller.
RED a controller is created - 2014-08-13 14:07
def test_play_move_calls_is_legal():
controller = NoughtsAndCrossesController()
To get back to green, it is a simple process of creating a controller class.
GREEN - 2014-08-13 14:08
def test_play_move_calls_is_legal():
controller = NoughtsAndCrossesController()
class NoughtsAndCrossesController:
pass
After we’d passed the test, we decided we didn’t like the test name, so we made it more specific to what we actually wanted to test.
REFACTOR better test name - 2014-08-13 14:10
def test_playing_legal_move_updates_view():
controller = NoughtsAndCrossesController()
class NoughtsAndCrossesController:
pass
Now we are green again, we continue to implement the test. Again, it fails to compile pretty quickly.
RED play_move - 2014-08-13 14:12
def test_playing_legal_move_updates_view():
controller = NoughtsAndCrossesController()
controller.play_move(0)
class NoughtsAndCrossesController:
pass
This time, we make the test pass by adding a method to our controller class.
GREEN - 2014-08-13 14:12
def test_playing_legal_move_updates_view():
controller = NoughtsAndCrossesController()
controller.play_move(0)
class NoughtsAndCrossesController:
def play_move(self, move):
pass
There’s nothing to refactor this time, as we’ve not really done anything. Therefore, we continue to write our test by making an assertion about a fake view’s add_move()
getting called with 0
.
RED test view is called - 2014-08-13 14:15
import mock
def test_playing_legal_move_updates_view():
fake_view = mock.Mock()
controller = NoughtsAndCrossesController()
controller.play_move(0)
fake_view.add_move.assert_called_with(0)
class NoughtsAndCrossesController:
def play_move(self, move):
pass
Making the test pass is a bit more involved this time. We have to inject the view into our controller and actually call the add_move()
method from the controller.
GREEN - 2014-08-13 14:17
import mock
def test_playing_legal_move_updates_view():
fake_view = mock.Mock()
controller = NoughtsAndCrossesController(fake_view)
controller.play_move(0)
fake_view.add_move.assert_called_with(0)
class NoughtsAndCrossesController:
def __init__(self, view):
self.view = view
def play_move(self, move):
self.view.add_move(move)
Again, there is nothing that we can see that’s worth refactoring at this stage.
We can now move on to our second test. We want to check that playing an illegal move does not add a move to our view (Ed: probably a bad name for the test here, as we may want to update the view by informing the user they’ve played an illegal move).
This initially fails as we aren’t accepting a model into our controller’s constructor.
RED added model to controller - 2014-08-13 14:21
import mock
def test_playing_legal_move_updates_view():
fake_view = mock.Mock()
controller = NoughtsAndCrossesController(fake_view)
controller.play_move(0)
fake_view.add_move.assert_called_with(0)
def test_playing_illegal_move_does_not_update_view():
fake_view = mock.Mock()
fake_model = mock.Mock()
fake_model.is_legal.return_value = False
controller = NoughtsAndCrossesController(fake_model, fake_view)
class NoughtsAndCrossesController:
def __init__(self, view):
self.view = view
def play_move(self, move):
self.view.add_move(move)
Passing is easy. We add a constructor parameter (and also update our other test).
GREEN - 2014-08-13 14:22
import mock
def test_playing_legal_move_updates_view():
fake_view = mock.Mock()
controller = NoughtsAndCrossesController(None, fake_view)
controller.play_move(0)
fake_view.add_move.assert_called_with(0)
def test_playing_illegal_move_does_not_update_view():
fake_view = mock.Mock()
fake_model = mock.Mock()
fake_model.is_legal.return_value = False
controller = NoughtsAndCrossesController(fake_model, fake_view)
class NoughtsAndCrossesController:
def __init__(self, model, view):
self.model = model
self.view = view
def play_move(self, move):
self.view.add_move(move)
We can then make the test fail again by calling the play_move()
method on the controller.
RED check illegal move does not update view - 2014-08-13 14:27
import mock
def test_playing_legal_move_updates_view():
fake_view = mock.Mock()
controller = NoughtsAndCrossesController(None, fake_view)
controller.play_move(0)
fake_view.add_move.assert_called_with(0)
def test_playing_illegal_move_does_not_update_view():
fake_view = mock.Mock()
fake_model = mock.Mock()
fake_model.is_legal.return_value = False
controller = NoughtsAndCrossesController(fake_model, fake_view)
controller.play_move(42)
assert not fake_view.add_move.called
class NoughtsAndCrossesController:
def __init__(self, model, view):
self.model = model
self.view = view
def play_move(self, move):
self.view.add_move(move)
Making the test pass is a simple case (as it should be) of checking that it is legal by calling the model’s is_legal()
method (which we’ve already mocked). Note that we also need to update our first test by passing a model.
GREEN - 2014-08-13 14:28
import mock
def test_playing_legal_move_updates_view():
fake_view = mock.Mock()
fake_model = mock.Mock()
controller = NoughtsAndCrossesController(fake_model, fake_view)
controller.play_move(0)
fake_view.add_move.assert_called_with(0)
def test_playing_illegal_move_does_not_update_view():
fake_view = mock.Mock()
fake_model = mock.Mock()
fake_model.is_legal.return_value = False
controller = NoughtsAndCrossesController(fake_model, fake_view)
controller.play_move(42)
assert not fake_view.add_move.called
class NoughtsAndCrossesController:
def __init__(self, model, view):
self.model = model
self.view = view
def play_move(self, move):
if self.model.is_legal(move):
self.view.add_move(move)
We can see that we’ve introduced quite a bit of duplication in these two tests, so we need to refactor. Most of the duplication can be removed by introducing a setup method to create our mocks and controller.
REFACTOR use a test class - 2014-09-23 12:48
import mock
class TestNoughtsAndCrosses():
def setup_method(self, method):
self.fake_view = mock.Mock()
self.fake_model = mock.Mock()
self.controller = NoughtsAndCrossesController(
self.fake_model,
self.fake_view)
def teardown_method(self, method):
pass
def test_playing_legal_move_updates_view(self):
self.controller.play_move(0)
self.fake_view.add_move.assert_called_with(0)
def test_playing_illegal_move_does_not_update_view(self):
self.fake_model.is_legal.return_value = False
self.controller.play_move(42)
assert not self.fake_view.add_move.called
class NoughtsAndCrossesController:
def __init__(self, model, view):
self.model = model
self.view = view
def play_move(self, move):
if self.model.is_legal(move):
self.view.add_move(move)
It is now much easier to add a new test. We now check that an illegal move results in an appropriate message to the view.
RED illegal move reports error - 2014-09-23 13:03
import mock
class TestNoughtsAndCrosses():
def setup_method(self, method):
self.fake_view = mock.Mock()
self.fake_model = mock.Mock()
self.controller = NoughtsAndCrossesController(
self.fake_model,
self.fake_view)
def teardown_method(self, method):
pass
def test_playing_legal_move_updates_view(self):
self.controller.play_move(0)
self.fake_view.add_move.assert_called_with(0)
def test_playing_illegal_move_does_not_update_view(self):
self.fake_model.is_legal.return_value = False
self.controller.play_move(42)
assert not self.fake_view.add_move.called
def test_playing_illegal_move_reports_error_in_view(self):
self.fake_model.is_legal.return_value = False
self.controller.play_move(-1)
self.fake_view.report_error.assert_called_with('Illegal move')
class NoughtsAndCrossesController:
def __init__(self, model, view):
self.model = model
self.view = view
def play_move(self, move):
if self.model.is_legal(move):
self.view.add_move(move)
Passing the test is straightforward.
GREEN - 2014-09-23 13:03
import mock
class TestNoughtsAndCrosses():
def setup_method(self, method):
self.fake_view = mock.Mock()
self.fake_model = mock.Mock()
self.controller = NoughtsAndCrossesController(
self.fake_model,
self.fake_view)
def teardown_method(self, method):
pass
def test_playing_legal_move_updates_view(self):
self.controller.play_move(0)
self.fake_view.add_move.assert_called_with(0)
def test_playing_illegal_move_does_not_update_view(self):
self.fake_model.is_legal.return_value = False
self.controller.play_move(42)
assert not self.fake_view.add_move.called
def test_playing_illegal_move_reports_error_in_view(self):
self.fake_model.is_legal.return_value = False
self.controller.play_move(-1)
self.fake_view.report_error.assert_called_with('Illegal move')
class NoughtsAndCrossesController:
def __init__(self, model, view):
self.model = model
self.view = view
def play_move(self, move):
if self.model.is_legal(move):
self.view.add_move(move)
else:
self.view.report_error('Illegal move')
Progress continues in the same vein until we have completed the controller. One could argue that we should really be working in thin slices of full-stack functionality but we decided that the task was simple enough to do as a single task.
Once the controller is complete, we move on to the model and then, finally, to the view.
Next Time
Next time we’ll look at the transitions to implementing the model and the view and some of the other interesting moments from the ‘Normal’ TDD.
Tags
programming and tdd