# Test-Driven Development in Python ``` $ nosetests ---------------------------------------------------------------------- Ran 0 tests in 0.001s OK ``` --- # First test; it's failing ```python # test_leaderboard.py import unittest import leaderboard class TestLeaderboard(unittest.TestCase): def test_empty(self): lb = leaderboard.Leaderboard() self.assertEqual(0, len(lb.leaders())) ``` ``` $ nosetests E ====================================================================== ERROR: Failure: ImportError (No module named leaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/nose/loader.py", line 411, in loadTestsFromName addr.filename, addr.module) File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/nose/importer.py", line 47, in importFromPath return self.importFromDir(dir_path, fqname) File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/nose/importer.py", line 94, in importFromDir mod = load_module(part_fqname, fh, filename, desc) File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 2, in
import leaderboard *ImportError: No module named leaderboard ---------------------------------------------------------------------- Ran 1 test in 0.021s FAILED (errors=1) ``` --- # Create leaderboard.py Just do the obvious thing to fix that error. Now we have a new oneā¦ ```python # leaderboard.py ``` ```python # test_leaderboard.py import unittest import leaderboard class TestLeaderboard(unittest.TestCase): def test_empty(self): lb = leaderboard.Leaderboard() self.assertEqual(0, len(lb.leaders())) ``` ``` $ nosetests E ====================================================================== ERROR: test_empty (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 6, in test_empty lb = leaderboard.Leaderboard() *AttributeError: 'module' object has no attribute 'Leaderboard' ---------------------------------------------------------------------- Ran 1 test in 0.014s FAILED (errors=1) ``` --- # Getting closer We have a `Leaderboard` now! But the test is still failing... ```python # leaderboard.py class Leaderboard(object): pass ``` ```python # test_leaderboard.py import unittest import leaderboard class TestLeaderboard(unittest.TestCase): def test_empty(self): lb = leaderboard.Leaderboard() self.assertEqual(0, len(lb.leaders())) ``` ``` $ nosetests E ====================================================================== ERROR: test_empty (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 7, in test_empty self.assertEqual(0, len(lb.leaders())) *AttributeError: 'Leaderboard' object has no attribute 'leaders' ---------------------------------------------------------------------- Ran 1 test in 0.027s FAILED (errors=1) ``` --- # Still closer nose is telling us that `leaders` needs to return something with a len(). What's the simplest possible thing we could do to make it happy? ```python # leaderboard.py class Leaderboard(object): def leaders(self): pass ``` ```python # test_leaderboard.py import unittest import leaderboard class TestLeaderboard(unittest.TestCase): def test_empty(self): lb = leaderboard.Leaderboard() self.assertEqual(0, len(lb.leaders())) ``` ``` $ nosetests E ====================================================================== ERROR: test_empty (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 7, in test_empty self.assertEqual(0, len(lb.leaders())) *TypeError: object of type 'NoneType' has no len() ---------------------------------------------------------------------- Ran 1 test in 0.014s FAILED (errors=1) ``` --- # Yay, our first passing test! We've done the bare minimum to get the test to pass, and nothing else. ```python # leaderboard.py class Leaderboard(object): def leaders(self): return [] ``` ```python # test_leaderboard.py import unittest import leaderboard class TestLeaderboard(unittest.TestCase): def test_empty(self): lb = leaderboard.Leaderboard() self.assertEqual(0, len(lb.leaders())) ``` ``` $ nosetests . ---------------------------------------------------------------------- Ran 1 test in 0.013s OK ``` --- # Let's add another test First we make sure that it fails, so we know it's really testing something. ```python # leaderboard.py class Leaderboard(object): def leaders(self): return [] ``` ```python # test_leaderboard.py @@ -5,3 +5,8 @@ class TestLeaderboard(unittest.TestCase): def test_empty(self): lb = leaderboard.Leaderboard() self.assertEqual(0, len(lb.leaders())) + + def test_has_one_leader(self): + lb = leaderboard.Leaderboard() + lb.track_score(5, object()) + self.assertEqual(1, len(lb.leaders())) ``` ``` $ nosetests .E ====================================================================== ERROR: test_has_one_leader (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 11, in test_has_one_leader lb.track_score(5, object()) *AttributeError: 'Leaderboard' object has no attribute 'track_score' ---------------------------------------------------------------------- Ran 2 tests in 0.022s FAILED (errors=1) ``` --- # A new error Now we can see the logic error in `leaders` in the failure message. ```python # leaderboard.py class Leaderboard(object): def leaders(self): return [] def track_score(self, score, obj): pass ``` ``` $ nosetests .F ====================================================================== FAIL: test_has_one_leader (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 12, in test_has_one_leader self.assertEqual(1, len(lb.leaders())) *AssertionError: 1 != 0 ---------------------------------------------------------------------- Ran 2 tests in 0.010s FAILED (failures=1) ``` --- # Tests pass! But it's still not doing what we want... ```python # leaderboard.py class Leaderboard(object): def __init__(self): self._x = 0 def leaders(self): return range(self._x) def track_score(self, score, obj): self._x = self._x + 1 ``` ``` $ nosetests .. ---------------------------------------------------------------------- Ran 2 tests in 0.007s OK ``` --- # Hopefully a stricter test? We want this test to work, but let's make sure. ```python # leaderboard.py class Leaderboard(object): def __init__(self): self._x = 0 def leaders(self): return range(self._x) def track_score(self, score, obj): self._x = self._x + 1 ``` ```python # test_leaderboard.py @@ -10,3 +10,9 @@ class TestLeaderboard(unittest.TestCase): lb = leaderboard.Leaderboard() lb.track_score(5, object()) self.assertEqual(1, len(lb.leaders())) + + def test_returns_leader_object(self): + lb = leaderboard.Leaderboard() + bob = object() + lb.track_score(1, bob) + self.assertEqual(bob, lb.leaders()[0]) ``` ``` $ nosetests ..F ====================================================================== FAIL: test_returns_leader_object (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 18, in test_returns_leader_object self.assertEqual(bob, lb.leaders()[0]) *AssertionError:
!= 0 ---------------------------------------------------------------------- Ran 3 tests in 0.030s FAILED (failures=1) ``` --- # Yep, still failing The test will only pass if we make `leaders` return what we gave it. ```python # leaderboard.py @@ -3,7 +3,7 @@ class Leaderboard(object): self._x = 0 def leaders(self): - return range(self._x) + return [object() for _ in range(self._x)] def track_score(self, score, obj): self._x = self._x + 1 ``` ``` $ nosetests ..F ====================================================================== FAIL: test_returns_leader_object (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 18, in test_returns_leader_object self.assertEqual(bob, lb.leaders()[0]) *AssertionError:
!=
---------------------------------------------------------------------- Ran 3 tests in 0.008s FAILED (failures=1) ``` --- # Tests pass again ```python # leaderboard.py class Leaderboard(object): def __init__(self): - self._x = 0 + self._leaders = [] def leaders(self): - return [object() for _ in range(self._x)] + return self._leaders def track_score(self, score, obj): - self._x = self._x + 1 + self._leaders.append(obj) ``` ``` $ nosetests ... ---------------------------------------------------------------------- Ran 3 tests in 0.010s OK ``` --- # New failing test ```python # test_leaderboard.py @@ -16,3 +16,9 @@ class TestLeaderboard(unittest.TestCase): bob = object() lb.track_score(1, bob) self.assertEqual(bob, lb.leaders()[0]) + + def test_stores_up_to_max(self): + lb = leaderboard.Leaderboard(max=5) + for i in range(5): + lb.track_score(2, object()) + self.assertEqual(i + 1, len(lb.leaders())) ``` ``` $ nosetests ...E ====================================================================== ERROR: test_stores_up_to_max (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 21, in test_stores_up_to_max lb = leaderboard.Leaderboard(max=5) *TypeError: __init__() got an unexpected keyword argument 'max' ---------------------------------------------------------------------- Ran 4 tests in 0.008s FAILED (errors=1) ``` --- # Tests pass ```python # leaderboard.py class Leaderboard(object): - def __init__(self): + def __init__(self, max=None): self._leaders = [] def leaders(self): ``` ``` $ nosetests .... ---------------------------------------------------------------------- Ran 4 tests in 0.026s OK ``` --- # Failing test ```python # test_leaderboard.py @@ -22,3 +22,9 @@ class TestLeaderboard(unittest.TestCase): for i in range(5): lb.track_score(2, object()) self.assertEqual(i + 1, len(lb.leaders())) + + def test_does_not_store_more_than_max(self): + lb = leaderboard.Leaderboard(max=5) + for i in range(6): + lb.track_score(2, object()) + self.assertEqual(5, len(lb.leaders())) ``` ``` $ nosetests F.... ====================================================================== FAIL: test_does_not_store_more_than_max (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 30, in test_does_not_store_more_than_max self.assertEqual(5, len(lb.leaders())) *AssertionError: 5 != 6 ---------------------------------------------------------------------- Ran 5 tests in 0.011s FAILED (failures=1) ``` --- # Tests passing ```python # leaderboard.py class Leaderboard(object): def __init__(self, max=None): self._leaders = [] + self._max = max def leaders(self): return self._leaders def track_score(self, score, obj): - self._leaders.append(obj) + if not self._max or len(self._leaders) < self._max: + self._leaders.append(obj) ``` ``` $ nosetests ..... ---------------------------------------------------------------------- Ran 5 tests in 0.015s OK ``` --- # Another failing test ```python # test_leaderboard.py @@ -28,3 +28,9 @@ class TestLeaderboard(unittest.TestCase): for i in range(6): lb.track_score(2, object()) self.assertEqual(5, len(lb.leaders())) + + def test_max_is_5_by_default(self): + lb = leaderboard.Leaderboard() + for i in range(6): + lb.track_score(2, object()) + self.assertEqual(5, len(lb.leaders())) ``` ``` $ nosetests ...F.. ====================================================================== FAIL: test_max_is_5_by_default (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 36, in test_max_is_5_by_default self.assertEqual(5, len(lb.leaders())) *AssertionError: 5 != 6 ---------------------------------------------------------------------- Ran 6 tests in 0.011s FAILED (failures=1) ``` --- # Passing ```python # leaderboard.py class Leaderboard(object): - def __init__(self, max=None): + def __init__(self, max=5): self._leaders = [] self._max = max @@ -7,5 +7,5 @@ class Leaderboard(object): return self._leaders def track_score(self, score, obj): - if not self._max or len(self._leaders) < self._max: + if len(self._leaders) < self._max: self._leaders.append(obj) ``` ``` $ nosetests ...... ---------------------------------------------------------------------- Ran 6 tests in 0.012s OK ``` --- # How would you implement this test? ```python # test_leaderboard.py @@ -34,3 +34,7 @@ class TestLeaderboard(unittest.TestCase): for i in range(6): lb.track_score(2, object()) self.assertEqual(5, len(lb.leaders())) + + @unittest.skip('TODO: implement me') + def test_max_0_is_unlimited(self): + pass ``` ``` $ nosetests ...S... ---------------------------------------------------------------------- Ran 7 tests in 0.012s OK (SKIP=1) ``` --- # How about this? ```python # test_leaderboard.py @@ -38,3 +38,7 @@ class TestLeaderboard(unittest.TestCase): @unittest.skip('TODO: implement me') def test_max_0_is_unlimited(self): pass + + @unittest.skip('TODO: implement me') + def test_sorts_by_score(self): + pass ``` ``` $ nosetests ...S..S. ---------------------------------------------------------------------- Ran 8 tests in 0.015s OK (SKIP=2) ``` --- # Add another test This time, the code and test changed in the same commit. You'd usually do it this way in a project, but we're breaking it down further for demonstration. ```python # leaderboard.py @@ -4,7 +4,7 @@ class Leaderboard(object): self._max = max def leaders(self): - return self._leaders + return list(self._leaders) def track_score(self, score, obj): if len(self._leaders) < self._max: ``` ```python # test_leaderboard.py @@ -42,3 +42,8 @@ class TestLeaderboard(unittest.TestCase): @unittest.skip('TODO: implement me') def test_sorts_by_score(self): pass + + def test_leaders_is_copy(self): + lb = leaderboard.Leaderboard() + lb.leaders().append(object()) + self.assertEqual(0, len(lb.leaders())) ``` ``` $ nosetests ....S..S. ---------------------------------------------------------------------- Ran 9 tests in 0.009s OK (SKIP=2) ``` --- # Test in the direction we want to go This test might make it easier to do `test_sorts_by_score`. ```python # test_leaderboard.py @@ -47,3 +47,9 @@ class TestLeaderboard(unittest.TestCase): lb = leaderboard.Leaderboard() lb.leaders().append(object()) self.assertEqual(0, len(lb.leaders())) + + def test_remembers_score(self): + lb = leaderboard.Leaderboard() + bob = object() + lb.track_score(5, bob) + self.assertEqual(5, lb.get_score(bob)) ``` ``` $ nosetests ....S.E.S. ====================================================================== ERROR: test_remembers_score (test_leaderboard.TestLeaderboard) ---------------------------------------------------------------------- Traceback (most recent call last): File "/private/tmp/repo.edcIBn/commit/test/test_leaderboard.py", line 55, in test_remembers_score self.assertEqual(5, lb.get_score(bob)) *AttributeError: 'Leaderboard' object has no attribute 'get_score' ---------------------------------------------------------------------- Ran 10 tests in 0.074s FAILED (SKIP=2, errors=1) ``` --- # Make the test pass Remember, always do the minimum to make the test pass! ```python # leaderboard.py @@ -9,3 +9,6 @@ class Leaderboard(object): def track_score(self, score, obj): if len(self._leaders) < self._max: self._leaders.append(obj) + + def get_score(self, obj): + return 5 ``` ``` $ nosetests ....S...S. ---------------------------------------------------------------------- Ran 10 tests in 0.034s OK (SKIP=2) ``` --- # Really, the minimum This makes our tests more robust. ```python # leaderboard.py @@ -9,6 +9,7 @@ class Leaderboard(object): def track_score(self, score, obj): if len(self._leaders) < self._max: self._leaders.append(obj) + self._last_score = score def get_score(self, obj): - return 5 + return self._last_score ``` ```python # test_leaderboard.py @@ -53,3 +53,7 @@ class TestLeaderboard(unittest.TestCase): bob = object() lb.track_score(5, bob) self.assertEqual(5, lb.get_score(bob)) + + dara = object() + lb.track_score(6, dara) + self.assertEqual(6, lb.get_score(dara)) ``` ``` $ nosetests ....S...S. ---------------------------------------------------------------------- Ran 10 tests in 0.028s OK (SKIP=2) ``` --- # There we go We can almost do `test_sorts_by_score`, but this seems inefficient. ```python # leaderboard.py @@ -4,12 +4,13 @@ class Leaderboard(object): self._max = max def leaders(self): - return list(self._leaders) + return [x['obj'] for x in self._leaders] def track_score(self, score, obj): if len(self._leaders) < self._max: - self._leaders.append(obj) - self._last_score = score + self._leaders.append({'score': score, 'obj': obj}) def get_score(self, obj): - return self._last_score + for x in self._leaders: + if x['obj'] == obj: + return x['score'] ``` ```python # test_leaderboard.py @@ -51,9 +51,9 @@ class TestLeaderboard(unittest.TestCase): def test_remembers_score(self): lb = leaderboard.Leaderboard() bob = object() - lb.track_score(5, bob) - self.assertEqual(5, lb.get_score(bob)) - dara = object() + + lb.track_score(5, bob) lb.track_score(6, dara) self.assertEqual(6, lb.get_score(dara)) + self.assertEqual(5, lb.get_score(bob)) ``` ``` $ nosetests ....S...S. ---------------------------------------------------------------------- Ran 10 tests in 0.023s OK (SKIP=2) ``` --- # Refactor to make get_score faster We know we haven't broken anything; the tests still pass! ```python # leaderboard.py class Leaderboard(object): def __init__(self, max=5): - self._leaders = [] + self._leaders = dict() self._max = max def leaders(self): - return [x['obj'] for x in self._leaders] + return [k for k, v in self._leaders.items()] def track_score(self, score, obj): if len(self._leaders) < self._max: - self._leaders.append({'score': score, 'obj': obj}) + self._leaders[obj] = score def get_score(self, obj): - for x in self._leaders: - if x['obj'] == obj: - return x['score'] + return self._leaders[obj] ``` ``` $ nosetests ....S...S. ---------------------------------------------------------------------- Ran 10 tests in 0.018s OK (SKIP=2) ``` --- # Another test to implement ```python # test_leaderboard.py @@ -57,3 +57,7 @@ class TestLeaderboard(unittest.TestCase): lb.track_score(6, dara) self.assertEqual(6, lb.get_score(dara)) self.assertEqual(5, lb.get_score(bob)) + + @unittest.skip('TODO: implement me') + def test_remembers_highest_score(self): + pass ``` ``` $ nosetests ....S.S..S. ---------------------------------------------------------------------- Ran 11 tests in 0.042s OK (SKIP=3) ```