Test-driven Development Fundamentals
Test-driven development (TDD) is a methodology that programmers use to produce higher quality code. The “typical” procedure of coding has been code first, test second. TDD swaps this mindset by focusing on testing before the actual coding happens. This article is a brief overview of the fundamentals with a small example of how it works. We will be using Python, and more specifically the unittest
framework provided in the standard Python library. Although Python is the language used in this example, the process of TDD is agnostic to any language.
The Process
The diagram shows the high-level steps for TDD. The goal is to do the absolute minimum amount of work as possible to make the test pass and to avoid overcomplicating the process. Using an example, we’ll step through each step to show how it would work. If you’re unfamiliar with the unittest
framework from Python you can check out this tutorial for a general overview (or you should be able to pick up a lot of the functionality from the comments in the code).
Example Overview
We want to create a script that formats a list of street addresses. Our main requirement is to ensure that there are no decimals in any address.
Step 1: Write Test
This first test will check to see if our addresses were successfully loaded into the list.
# File: "test.py"
import unittest # Import the unittest framework
from my_code_block import format_addresses # Import the function from our script file
# Create our own test case class that inherits from the base TestCase class
class AddressTestCase(unittest.TestCase):
# Do an intial setup to make the addresses available to all tests
def setUp(self):
self.addresses = ['634 Tomato Way', '233 E. 500 S.', '1800 N. Python Lane']
def test_address_in_list(self):
# This assert checks that the first address is in the list by seeing if it's equal
first_address = format_addresses(self.addresses)[0]
self.assertEqual(first_address, '634 Tomato Way')
Step 2: Run and Fail Test
The result is that the test failed, which is expected as we haven’t written anything yet.
$ python -m unittest
======================================================================
ERROR: test_address_in_list (__main__.AddressTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File 'test.py', line 12, in test_address_in_list
first_address = format_addresses(self.addresses)[0]
NameError: name 'format_addresses' is not defined
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
Step 3: Write Code
Now we can actually write the code to make it succeed.
# File: "my_code_block.py"
def format_addresses(addresses):
return addresses
Step 4: Run and Pass Test
$ python -m unittest
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Step 5: Refactor Code
At this point there’s not much code to refactor, seeing it was a very arbitrary example, but after doing each round it’s good to take a second to see what pieces of code you can consolidate and make more efficient.
Now repeat!
We’ll now apply the same principles to format the addresses to not have any decimals in any them.
Step 1: Write Test
# File: "test.py"
import unittest
from my_code_block import format_addresses
class AddressTestCase(unittest.TestCase):
# Do an intial setup to make the addresses available to all tests
def setUp(self):
self.addresses = ['634 Tomato Way', '233 E. 500 S.', '1800 N. Python Lane']
def test_address_in_list(self):
first_address = format_addresses(self.addresses)[0]
self.assertEqual(first_address, '634 Tomato Way')
# ***************** Added decimal test ********************
def test_decimal(self):
no_decimals = True
formatted_addresses = format_addresses(self.addresses)
for value in formatted_addresses:
if '.' in value:
no_decimals = False
# Run an assert on whether or not there were decimals in one of the addresses.
self.assertTrue(no_decimals)
Step 2: Run and Fail Test
$ python -m unittest
======================================================================
FAIL: test_decimal (__main__.AddressTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File 'test.py', line 23, in test_decimal
self.assertTrue(no_decimals)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Step 3: Write Code
# File: "my_code_block.py"
def format_addresses(addresses):
formatted_addresses = []
for value in addresses:
value = value.replace('.', '')
formatted_addresses.append(value)
return formatted_addresses
Step 4: Run and Pass Test
$ python -m unittest
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Step 5: Refactor Code
Then just double check for ways to improve your code.
Repeat!
It was hard for me to get my mind around this. It seemed backward to my nature to test first. But once I did a few projects using this new paradigm, I felt my code became much cleaner. TDD allows you to fix problems before they even exist.