Agile and Test-Driven Development

TDD Worked Example

Robert Haines, University of Manchester, UK

Adapted from "Test-Driven Development By Example", Kent Beck

Introduction

  • Very simple example
  • Implement a function to return the nth number in the Fibonacci sequence

$ F_0 = 0, \\ F_1 = 1, \\ F_n = F_{n-1} + F_{n-2} $

Step 0a: Local python setup

You need to do this if you're using python on the command line.

  • Create two directories
    • src
    • test
  • Add the src directory to PYTHONPATH
    $ export PYTHONPATH=`pwd`/src

Step 0b: IPython setup

You need to do this if you're using this IPython Notebook.

The run_tests() method, below, is called at the end of each step to run the tests.


In [ ]:
import unittest

def run_tests():
    suite = unittest.TestLoader().loadTestsFromTestCase(TestFibonacci)
    unittest.TextTestRunner().run(suite)

Step 1: Write a test (and run it)


In [ ]:
class TestFibonacci(unittest.TestCase):

    def test_fibonacci(self):
        self.assertEqual(0, fibonacci(0), "fibonacci(0) should equal 0")

run_tests()

Step 1: Implement and re-test


In [ ]:
def fibonacci(n):
    return 0

run_tests()

Step 2: Write a test (and run it)


In [ ]:
class TestFibonacci(unittest.TestCase):

    def test_fibonacci(self):
        self.assertEqual(0, fibonacci(0), "fibonacci(0) should equal 0")
        self.assertEqual(1, fibonacci(1), "fibonacci(1) should equal 1")

run_tests()

Step 2: Implement and re-test


In [ ]:
def fibonacci(n):
    if n == 0: return 0
    return 1

run_tests()

Step 3: Write a test (and run it)


In [ ]:
class TestFibonacci(unittest.TestCase):

    def test_fibonacci(self):
        self.assertEqual(0, fibonacci(0), "fibonacci(0) should equal 0")
        self.assertEqual(1, fibonacci(1), "fibonacci(1) should equal 1")
        self.assertEqual(1, fibonacci(2), "fibonacci(2) should equal 1")

run_tests()

Step 3: It works!

The current code outputs 1 whenever n is not 0. So this behaviour is correct.

Step 4: Write a test (and run it)


In [ ]:
class TestFibonacci(unittest.TestCase):

    def test_fibonacci(self):
        self.assertEqual(0, fibonacci(0), "fibonacci(0) should equal 0")
        self.assertEqual(1, fibonacci(1), "fibonacci(1) should equal 1")
        self.assertEqual(1, fibonacci(2), "fibonacci(2) should equal 1")
        self.assertEqual(2, fibonacci(3), "fibonacci(3) should equal 2")

run_tests()

Step 4: Implement and re-test


In [ ]:
def fibonacci(n):
    if n == 0: return 0
    if n <= 2: return 1
    return 2

run_tests()

Step 5: Write a test (and run it)


In [ ]:
class TestFibonacci(unittest.TestCase):

    def test_fibonacci(self):
        self.assertEqual(0, fibonacci(0), "fibonacci(0) should equal 0")
        self.assertEqual(1, fibonacci(1), "fibonacci(1) should equal 1")
        self.assertEqual(1, fibonacci(2), "fibonacci(2) should equal 1")
        self.assertEqual(2, fibonacci(3), "fibonacci(3) should equal 2")
        self.assertEqual(3, fibonacci(4), "fibonacci(4) should equal 3")

run_tests()

Step 5: Implement and re-test


In [ ]:
def fibonacci(n):
    if n == 0: return 0
    if n <= 2: return 1
    if n == 3: return 2
    return 3

run_tests()

Pause

  • How many tests are we going to write?
  • Just how big is the set of if statements going to get if we carry on like this?
  • Where do we stop?

Remember:

$ F_0 = 0, \\ F_1 = 1, \\ F_n = F_{n-1} + F_{n-2} $

Can we reflect that in the code?

Step 6: Refactor and test


In [ ]:
def fibonacci(n):
    if n == 0: return 0
    if n <= 2: return 1
    if n == 3: return 2
    return 2 + 1

run_tests()

Step 7: Refactor and test


In [ ]:
def fibonacci(n):
    if n == 0: return 0
    if n <= 2: return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

run_tests()

Step 8: Refactor and test (and done)


In [ ]:
def fibonacci(n):
    if n == 0: return 0
    if n == 1: return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

run_tests()

The end