Test-Driven Development with Python: a Primer
Published: 2023-09-16 21:12:05


---
title: Test-Driven Development with Python: a Primer
description: Making sure the software we build works the way we (and our customers) want it to work is called, unsurprisingly, software testing. Software testing is an enormous topic; indeed, there are entire books, courses, conferences, academic journals, and more about the topic. One can even make a career out of testing software. We couldn’t possibly [&hellip;]
canonical_url: https://erikscode.space/index.php/2023/09/16/test-driven-development-with-python-a-primer/
cover_image: https://i0.wp.com/erikscode.space/wp-content/uploads/2023/09/snake-ball-python-python-regius-beauty-53140.jpeg?fit=1880%2C1295&ssl=1
---

Making sure the software we build works the way we (and our customers) want it to work is called, unsurprisingly, software testing. Software testing is an enormous topic; indeed, there are entire books, courses, conferences, academic journals, and more about the topic. One can even make a career out of testing software. We couldn’t possibly scratch the surface of the complexity involved in software testing in this article, so we’ll only focus on a topic most relevant to us as programmers: test-driven development.

What is Test-Driven Development?
--------------------------------

To put it simply, test-driven development, or *TDD*, is a programming style in which a component’s tests are the primary mechanism guiding its development–this is often accomplished by writing tests ***before*** writing feature code. This is in contrast to the way software is traditionally built, where a feature is built end-to-end and then testing is added later as an afterthought.

There are several reasons for taking a test-first approach to building software. Most importantly, it forces you to think before you start writing code. Pondering how you might test something you’ve not written yet will necessarily make you imagine how you intend to build it, what you want it to do, and how other parts of the code might interact with it. Writing tests first also helps flush out unanswered questions you hadn’t yet thought about (for example, should a math function gracefully handle bad datatype inputs or fail loudly? What kind of data should a function expect as input? And so on).

One practical reason for writing tests before application code is that after a while, you have a fairly comprehensive test suite that reports on the functionality of most—if not all—of your system. This is a lifesaver as your projects get more complex and the possibility of a minor change in one part of the code breaking a feature in another area increases.

Now you may be wondering what it means to “write” a test. In the context of TDD, the word test is almost always shorthand for unit test—a program that checks the functionality of one small piece of functionality (a unit). If that sounds like it means we write code to test code, it’s because we do! In Python, unit tests are classes and methods that we build in order to verify the performance of other classes and methods that we build. Let’s first learn how to write unit tests before diving in to the TDD workflow.

Getting Started with Unit Testing in Python
-------------------------------------------

As their name suggests, unit tests test one unit of code—that could be a function’s return value, the attributes of a class, or even a single unit of functionality like the ability to login to a website. We don’t have to get hung up on the semantics right now, let’s just start writing. Create a file called *add\_fucntion.py* and write this function in it:

```python
def add(x, y):
  return x + y
```


The above function simply takes two arguments and returns their sum (adds them together). Now, suppose we wanted to test this function to make sure it works. We might open an interactive Python shell and make sure it returns the values we expect based on the values we pass to it:

```
>>> from add_function import add
>>> add(5, 5)
10
>>> add(4, -2)
2
>>> add(True, "hello")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "path/to/add_function.py", line 2, in add
    return x + y
```


In the above session, we imported the `add` function from `add_function`. We passed 5 and 5 to the function to see if we got 10 as expected; we pass a 4 and a -2 to make sure the function handles signed and unsigned integers appropriately; and finally, we pass the Boolean `True` and string `hello` to the function to make sure it throws an error. All inputs behave as expected, so we can be pretty sure the `add` function behaves as expected.

This is fine for this function since it’s small and simple, but what if we have much larger functions or functions that change a lot? Are we going to manually test every function in a Python shell like this? As you might expect, the answer is no!

This is where unit testing comes in, we’ll write a program that will run all these different lines and more. Python has two popular testing frameworks: `pytest` and `unittest`. The `unittest` module is part of the standard library, meaning we don’t have to do anything special to install it, so we’ll use it for the examples in this chapter.

To get started with `unittest`, make a file in the same directory as *add\_function.py* and call it *test\_add\_function.py*. Inside this new file we first need to import the `unittest` module and the file with the `add` function in it. To do that, make the top of the *test\_add\_function.py* file look like this:

```python
import unittest

from add_function import add
```


Now, with the `add` function and `unittest` module imported, it’s time to write our first test. With `unittest`, we start by first defining a test class. Test classes are usually a collection of tests for one conceptual level of functionality; in this case, the `add` function. Let’s call ours `AddTest`:

```python
class AddTest(unittest.TestCase):
```


Notice that the test class inherits the `TestCase` class from `unittest`. This gives us access to the `assert` methods we’ll use in a few minutes. Next, we’ll define a test method. Test methods are what make up test classes and we usually write one for each of the different ways our code might be used. For example, the most likely use of our add function is probably to add positive numbers, so let’s write a test method for that:

```python
class AddTest(unittest.TestCase):
  def test_positive_addition(self):
```


In the above code, all we did was add the test method `test_positive_addition` to the `AddTest` test class. Now we’re ready to write the actual assertions. In this case, we’ll pass the `add` function two numbers, save its return value in a variable called `actual`, and compare that with a variable called `expected` (the number that should be returned). Then, we’ll use the `assertEqual` method from `unittest` to test that the two values are the same. Here’s how your entire `test_add_funciton.py` file should like by now:

```python
import unittest

from add_function import add
class AddTest(unittest.TestCase):
  def test_positive_addition(self):
    expected = 30
    actual = add(10, 20)
    self.assertEqual(actual, expected)
```


Notice inside of the test method `test_positive_addition` we create two variables, `expected` with the value of 30, and `actual` with the return value of `add` when passed 10 and 20. Finally, we call the `assertEqual` class method and pass it `actual` and `expected`. As you might guess from its name, this method checks if the two values passed to it are equal.

You’ve now written your first unit test! It’s time to actually run the test. Open a terminal and, from whatever folder your `add_function.py `and `test_add_function.py` files are in, run the following command:

```
python -m unittest
```


This runs the `unittest` program which will find any file that starts with the word test and runs the assertions inside. You should see output similar to the following:

```
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
```


You might not see it, but the single dot at the top of the terminal output represents a test method that passed. Had the test failed, you would have seen an F. This segues nicely into an important point: you should always make sure your tests can fail. We actually never saw our test fail so how can we be 100% certain that our unit test is working?

To make sure our unit test is working the way we expect, let’s change the `expected` variable to 40 and see what happens. Make this change in your code:

```python
expected = 40
```


And run `python -m unittest` in your console once again. You should see something like the following output:

```
F

==================================================================

FAIL: test_positive_addition (test_add_function.AddTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/path/to/test_add_function.py", line 9, in test_positive_addition
    self.assertEqual(actual, expected)

AssertionError: 30 != 40
----------------------------------------------------------------------

Ran 1 test in 0.001s
FAILED (failures=1)
```


This is very interesting output, not only do we see that our test is testing the right thing (which we can see by the FAILED message when we tried to see if 20 + 10 could equal 40), but we also see what it looks like when a test fails. Notice that the output includes the file name and test method that failed as well as the line number of the assertion that caused the failure. This is super helpful when running big test suites (collections of automated tests like this one) with hundreds or thousands of tests.

Notice too that the failure message also tells us what failed. Specifically, it tells us that 30 != 40, which is exactly what we put into the test to force a failure. Go ahead and change the `expected` variable back to 30.

Now that we know how to write unit tests, let’s write a couple more. First, let’s test the method’s ability to handle negative numbers. We’ll also write this test specifically to fail. Check it out:

```python
def test_negative_addition(self):
    expected = 0
    actual = add(10, -20)
    self.assertEqual(actual, expected)
```


Here we’ve defined another test method under the `AddTest` test class. This time, we are using negative numbers to see if the add function handles numbers lower than zero. We’ll test this by passing 10 and -20 to the add function, but instead of setting the `expected` variable to -10, we set it to 0. We do this because we want to make sure the unit test we just wrote is testing the right thing, and the best way to do that is by forcing an assertion failure. Once again, go ahead and run `python -m unittest` in your terminal, you should see something like the following:

```
F.

==================================================================
FAIL: test_negative_addition (test_add_function.AddTest)
----------------------------------------------------------------------

Traceback (most recent call last):
  File "path/to/test_add_function.py", line 14, in test_negative_addition
    self.assertEqual(actual, expected)

AssertionError: -10 != 0
----------------------------------------------------------------------

Ran 2 tests in 0.001s
FAILED (failures=1)
```


Notice this time that the first line of output has a . followed by an F. This means that one of the test methods failed while another passed. As we hoped, the rest of the output tells us that our `test_negative_addition` method failed because -10 does not equal 0. Go ahead and change the `expected` variable to -10 so that the tests will pass.

Let’s write a slightly different test now. In the previous examples, we asserted two values were equal. This is of course an important kind of test to run but that’s not the only thing functions ever do. Remember earlier we tested what would happen by calling the `add` function by passing it `True` and the string hello and we saw that it raised a `TypeError`? We can actually test for that behavior too, check it out:

```python
def test_bad_datatypes(self):
    with self.assertRaises(TypeError):
      add(True, "hello")
```


In the above test method, we start by using the `with` keyword in combination with the `unittest` assert method `assertRaises` and pass it `TypeError`. Finally, inside the `with` block, we call the `add` function and pass `True` and the string hello. We have to use the `with` block because otherwise the method will error (even though it’s the error we’re testing for).

If you run this, it should pass. For the sake of time, I did not show you how to make this test intentionally fail. I leave that as an exercise to the reader.

Aside from `assertTrue` and `assertRaises`, `unittest` has many other assertion methods such as `assertAlmostEqual` for approximating, `assertIn` for checking the presence of a single element in a list, `assertFalse` for checking things you expect to be untrue, and many more.

Now that we’ve learned the basics of writing unit tests in Python with `unittest`, let’s talk about the flow of TDD.

The TDD Flow
------------

As I mentioned at the beginning of this article, test-driven development often consists of writing tests before writing actual code, but that’s just the beginning. The TDD workflow follows a three-phase cycle: write a failing test, write just enough code to make that test pass, refactor. You repeat this cycle until the code is doing what it’s meant to do, is thoroughly tested, and is as well designed as it can be. To understand the TDD workflow, we’ll start with analyzing the requirements and then repeating the cycle a couple of times.

### The Requirements

Imagine we are building a cash register application for our favorite coffee shop. This application currently consists of a `Drink` class, representing the many different drinks the coffee shop serves, an `Order` class that records a customer’s orders, and an `OrderItem` class representing an individual item in an order. These three simple classes are in a file called *coffee\_cash\_register.py* and look like this:

```python
class Drink:
  def __init__(self, name, price):
    self.name = name
    self.price = price

class OrderItem:
  def __init__(self, drink):
    self.drink = drink

class Order:
  def __init__(self, items):
    self.items = items

  def add_item(self, item):
    self.items.append(item)
```


The above class hierarchy consists of a `Drink` class which contains just a constructor and `name` and `price` attributes, an `OrderItem` class which simply contains a `drink` attribute, and an `Order` class that contains a list of `OrderItem` instances and a method for adding more `OrderItem` objects to that list.

We now want to build a class called `CashRegister` that serves as an way to access the `Order` class and allows an employee to create orders and build receipts. Since we are doing this the TDD way, our first step is to write a failing test for the `CashRegister` class.

### Write a Failing Test

To write our first test for `CashRegister`, we first have to think about what kind of attributes and features the `CashRegister` class should have. Let’s start by thinking about what a real-world cash register does. It allows an employee to build an order by adding items a customer wants to purchase or removing items if one was accidentally added to the order. It should also be able to calculate the total price for the entire order, so the employee knows how much to charge the customer.

This thought process has revealed three things the `CashRegister` class must be able to do, and thus three test methods. Create a file called *test\_cash\_register.py* and write the following code:

```python
import unittest

from coffee_cash_register import *

class CashRegisterTest(unittest.TestCase):
  def test_add_item_to_order(self):
    cr = CashRegister()

  def test_remove_item_from_order(self):
    cr = CashRegister()

  def test_calculate_total(self):
    cr = CashRegister()
```


Now remember, you want to write just enough of the test that the test should fail. We currently have three test methods, each creating a new instance of the `CashRegister` class—a class we haven’t even created yet. Run `python -m unittest` in the directory you’ve created these files and see that you have three failures with messages like `name CashRegister is not defined`. We’ve successfully completed the first part of our TDD cycle, writing a failing test.

### Get the Test to Pass

Now that we have our test class and three test methods created and failing, it’s time to get them to pass. Since our tests failed because `CashRegister` wasn’t defined, let’s go ahead and create that class. In *coffee\_cash\_register.py*, add the following class:

```python
class CashRegister:
  def __init__(self):
    pass
```


Here, we’ve created the `CashRegister` class with a very basic constructor. If you run the tests again, you’ll see that they pass. Believe it or not, that means we’re actually done with the second part of the TDD cycle: getting the test to pass. Let’s move on to the next step.

### Refactor

Before we return to the start of the TDD cycle, we need to refactor the code we’ve written. This includes both the test code and the application code; but right at this moment in our particular example, the only thing that needs refactoring is our test code. Notice how we instantiated a `CashRegister` object at the beginning of each test method. In order to save us from retyping the same line for every test method, let’s just make `cr` a class attribute:

```python
class CashRegisterTest(unittest.TestCase):
  cr = CashRegister()

 def test_add_item_to_order(self):
    pass

  def test_remove_item_from_order(self):
    pass

  def test_calculate_total(self):
    pass
```


Notice that we defined an attribute for the test class called `cr` and instantiated it with `CashRegister()`. Now we don’t have to write this line at the beginning of every test method. Notice also that we replaced the line inside each test method with `pass`, this is simply because since we no longer need to instantiate a `CashRegister` object, there is nothing to write in the test methods. If we leave them empty, our subsequent test runs will fail.

Now, let’s repeat the TDD cycle a couple more times to really get a feel for how this works.

### Repeat

We’ve now completed one iteration of the TDD cycle. The next step is to write another failing test. Actually, we’ll just add to one of our existing tests and get it to fail. Let’s write the test code for the test method `test_add_item_to_order`. Again, we have to start by thinking about how this might work. We decide that adding an item to an order will most likely be done by creating a method in `CashRegister` that takes an instance of `Drink`, turns it into an `OrderItem`, and adds it to an `Order` object’s `items` attribute. We write the test method as such:

```python
def test_add_item_to_order(self):
    drink = Drink("Latte", 3.49)
    self.cr.add_item_to_order(drink)  
```


Now, if we run the tests, we should see a failure from this test method because the `CashRegister` object has no attribute `add_item_to_order`. The next step in the cycle is to get the test to pass, which right now means building that method for the `CashRegister` class. We might start by writing a method in `CashRegister` that would look something like:

```python
def add_item_to_order(self, drink):
    item = OrderItem(drink)
```


But now that we’ve written this line and are wondering how to add it to an order, we realize that neither the method nor the class actually has an `Order` object to add the `OrderItem` to. Now we have to think about how we plan to instantiate the idea of an Order into our `CashRegister` app. Do we make it a class attribute of the `CashRegister` class? Do we make `CashRegister` inherit `Order`? Neither of these approaches sound quite right since a cash register in real life is mostly independent from the orders it builds. We decide instead to pass an instance of `Order` to the `add_item_to_order` method. Check it out:

```python
def add_item_to_order(self, drink, order):
    item = OrderItem(drink)
```


In the method above, we’ve updated the argument list to take in an `order` parameter—an instance of the `Order` class. This will allow us to add the `OrderItem` instance to an actual `Order` instance. We also need to update our test code to account for the new method parameter:

```python
def test_add_item_to_order(self):
    drink = Drink("Latte", 3.49)
    order = Order([])
    self.cr.add_item_to_order(drink, order)
```


Now that we’ve instantiated an empty `Order` object, we can call the `add_item_to_order` method without breaking anything. This means we’ve completed the “get test to pass” portion of the TDD cycle once again! The next step should be to refactor the code but, in this case, there is not really much to refactor, so we start the cycle over once again.

We’re now starting iteration three of the TDD cycle and once again, our first task is to get the test to fail. The best way to accomplish that at this point is to write our first assertion. Since the `add_item_to_order` method is supposed to add an `Item` object to an `Order` object’s `items` attribute, and since we are instantiating an `Order` object with an empty `items` attribute in our test, we can assert that this method is working as intended by checking that the length of `order.items` is greater than 0. Check it out:

```python
def test_add_item_to_order(self):
    drink = Drink("Latte", 3.49)
    order = Order([])
    self.cr.add_item_to_order(drink, order)
    self.assertTrue(len(order.items) > 0)
```


Notice the line we added, `self.assertTrue(len(order.items) > 0)`. We figure if the `add_item_to_order` method is working like it should, the `order.items` list will have one item in it. If we run this test, it should fail with a message like `False is not True`. This fails because, in our application code, we never add the `OrderItem` object to the `Order` object’s items attribute. We’re again done with the first part of the TDD cycle. Let’s complete the second part of the TDD cycle by fixing the `add_item_to_order` method:

```python
def add_item_to_order(self, drink, order):
    item = OrderItem(drink)
    order.add_item(item)
```


We make this test pass by adding a call to the `Order` object’s `add_item` method which adds the `item` variable to the `order` variable’s `items` attribute. If you run the tests again, you should see that this one now passes. It’s time for the refactor stage.

Now that we think about it, the test we wrote is a little weak. The length of the `order` variable’s `items` attribute is only a side effect of what we really wanted to do which was add a `LineItem` containing the `drink` variable to the `order` variable’s `items` attribute. By only testing the length of `items`, the `add_item_to_order` method could add anything at all to the `items` attribute and our test would say that it’s passing. For the refactor stage, let’s be a bit more explicit about what we’re testing:

```python
def test_add_item_to_order(self):
    drink = Drink("Latte", 3.49)
    order = Order([])
    self.cr.add_item_to_order(drink, order)
    item = order.items[0]
    self.assertIsInstance(item, OrderItem)
    self.assertEqual(item.drink, drink)
```


We’ve rewritten the test method to be more explicit. First, we extract the first object out of the `order` variable’s `items` attribute after calling the `add_item_to_order` method. We expect that the item should be an `OrderItem` object, so we use the `unittest``assertIsInstance` method to make sure it is. Then, we use the `assertEqual` method to make sure that the `item` variable’s `drink` attribute is the `Drink` instance we created earlier in the test. This is a much more explicit test of the functionality we want because not only will it tell us if the method incorrectly created the `OrderItem`, it will let us know if we somehow accidentally appended the wrong kind of object to the `Order` instance’s `items` list.

It looks like this part of the class is pretty thoroughly tested now and we’re confident that the `CashRegister` class’s `add_item_to_order` method works as expected. You’ve now completed your first TDD workflow! Practice your understanding of this flow by repeating the steps for the other two test methods. I’ll give you a hint, to write a failing test for `test_remove_item_from_order`, you’ll probably have to call a method you haven’t written yet.

Conclusion
----------

This article gently introduces the world of software testing by exploring the concept of test-driven development (TDD). We started by defining what TDD is and why it’s helpful. We highlighted the fact that TDD not only helps us think about the software we build, but it also has the side effect of leaving us with a collection of tests that we can always check our software against. Then, we took a quick detour to talk about how to use Python’s `unittest` testing framework so we could learn what it means to “write” tests. We did this so we could practice the TDD workflow with an example. In our example we ran through the TDD cycle three times to fully build out one thorough test method for our `CashRegister` class.

To learn more about TDD, I suggest you start trying to use the flow in your daily programming work. At first, you will likely be less productive than you are used to, but as you practice more, you may be more productive than you’ve ever been! If you are primarily a Python programmer, I suggest you install `pytest` and learn how to use that instead of `unittest`. The `pytest` package is much more popular in Python projects, we started with `unittest` so we could skip the tedium of installing a new package.