Test-Driven Development – How Do I Start?

Posted: July 22nd, 2010 | Author: | Filed under: Python, Tutorials | 2 Comments »

There seem to be a lot of developers who like the idea of Test-Driven Development (TDD) and can clearly see the benefit of having tests written for their code but can’t seem to get their head around the process. How do you start writing unit tests before writing the actual code?

Let’s start with an example. You want to write a method that takes a URL as an argument and have it tell you through a boolean return if it’s the correct domain or not. It seems simple enough. Just write your method, pass the URL through some regular expression and you’re done.

But you yourself already know which domains are allowed and which are not so before writing the actual code you can run some tests in your head. E.g. I only want www.bbc.co.uk and its sub-domains on http and https. Nothing else should be allowed. So https://beta.bbc.co.uk/iplayer should return TRUE and http://www.bbcbb.com should return FALSE etc.

The process behind TDD is that you first write a failing test. Then you write the actual code and adjust until the test passes.

So let’s write some tests for our domain checker. I’m using Python and Unittest here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import unittest
 
# this is what we're going to be testing
class Utils():
    def is_bbc(self, val):
        pass #placeholder
 
# this is the actual test
class TestUtils(unittest.TestCase):
    def setUp(self):
        self.u = Utils()
 
    def test_is_bbc(self):
        self.assertTrue(self.u.is_bbc('http://www.bbc.co.uk/iplayer'))
        self.assertTrue(self.u.is_bbc('http://www.bbc.co.uk/food'))
        self.assertTrue(self.u.is_bbc('http://www.bbc.co.uk'))
        self.assertTrue(self.u.is_bbc('https://www.bbc.co.uk'))
        self.assertTrue(self.u.is_bbc('http://beta.bbc.co.uk'))
        self.assertFalse(self.u.is_bbc('http://www.bbc.com'))
        self.assertFalse(self.u.is_bbc('http://www.bbbc.co.uk'))
        self.assertFalse(self.u.is_bbc('http://.bbc.co.uk'))
 
suite = unittest.TestLoader().loadTestsFromTestCase(TestUtils)
unittest.TextTestRunner(verbosity=2).run(suite)

I’ve created a an empty method where our domain checker is going to live but as you can see it doesn’t do anything. The tests should immediately make sense. We pass a bunch of domain variations and we know which ones should pass or fail. Naturally, running the test right now will fail:

$ python sample.py
test_is_bbc (__main__.TestUtils) ... FAIL
 
======================================================================
FAIL: test_is_bbc (__main__.TestUtils)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "sample.py", line 14, in test_is_bbc
    self.assertTrue(self.u.is_bbc('http://www.bbc.co.uk/iplayer'))
AssertionError
 
----------------------------------------------------------------------
Ran 1 test in 0.000s
 
FAILED (failures=1)

Now you can start writing the actual code and keep running the same tests until it passes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import unittest
import re
 
# this is what we're going to be testing
class Utils():
    def is_bbc(self, val):
        return re.match('^https?://([^/]+)?\.bbc\.co\.uk', val)
 
# this is the actual test
class TestUtils(unittest.TestCase):
    def setUp(self):
        self.u = Utils()
 
    def test_is_bbc(self):
        self.assertTrue(self.u.is_bbc('http://www.bbc.co.uk/iplayer'))
        self.assertTrue(self.u.is_bbc('http://www.bbc.co.uk/food'))
        self.assertTrue(self.u.is_bbc('http://www.bbc.co.uk'))
        self.assertTrue(self.u.is_bbc('https://www.bbc.co.uk'))
        self.assertTrue(self.u.is_bbc('http://beta.bbc.co.uk'))
        self.assertFalse(self.u.is_bbc('http://www.bbc.com'))
        self.assertFalse(self.u.is_bbc('http://www.bbbc.co.uk'))
        self.assertFalse(self.u.is_bbc('http://.bbc.co.uk')) #this should fail
 
suite = unittest.TestLoader().loadTestsFromTestCase(TestUtils)
unittest.TextTestRunner(verbosity=2).run(suite)

The regex may look like it’s correct but running the test will fail again:

$ python sample.py
test_is_bbc (__main__.TestUtils) ... FAIL
 
======================================================================
FAIL: test_is_bbc (__main__.TestUtils)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "sample.py", line 22, in test_is_bbc
    self.assertFalse(self.u.is_bbc('http://.bbc.co.uk'))
AssertionError
 
----------------------------------------------------------------------
Ran 1 test in 0.001s
 
FAILED (failures=1)

It has failed on the final assert because our code will also allow http://.bbc.co.uk and we obviously don’t want that. But as you can see we’ve caught this edge case before deploying our app so we can promptly fix our code.

Hopefully this example demonstrates why it’s a good idea to start with tests. This is obviously a simple example but on bigger projects predicting the outcome of your system can save you a lot of debugging time in the future.

2 Comments »
  • http://www.norestfortheweekend.com Mark Stickley

    Nice post, thanks!

    I think the hardest part for me is thinking up the different tests. I mean, when you’re passing in a string, an array or even an object it could literally contain anything and yet you can’t write tests for infinite numbers of possibilities.

    Sure, I get that if you’re expecting a URL it’s kind of silly to write tests that pass an XML string instead, but I think what concerns me is that if I don’t think of all the possible edge cases (of when there can be many) then bugs are still going to creep in regardless of the unit tests.

    I’m not saying that TDD isn’t a good idea, but would you agree that unit tests are more useful for spotting regressions than for actually building flawless code from the start? It kinda feels that way to me.

  • http://www.givp.org Giv

    Hi Mark,

    You raise a good point about the possibility of different data types being passed to your methods. In my opinion there are two solutions. One is that languages like Java make you specify the data type for each argument and failing to use the correct data type will result in an exception. This is easy to catch and test.

    The second option is not to worry about the data type. In the above example you can pass an integer, array etc and the regex will handle the rest. So you don’t need to write a test for every data type. Your code should handle that and know the exact type it expects and fail gracefully or throw a meaningful exception if it receives the wrong input.

    You’re right about unit tests being useful for regression but it can definitely serve both purposes. Obviously you can tweak your tests later. Just because you wrote your tests upfront it doesn’t mean you can revise it later. The point is to think through most edge cases first to help you architect and start your code and by doing so you reduce the chance of having buggy code.