

Unit testing is one of the most important forms of automated software testing. That’s true for any programming language: Java, JavaScript, you name it. Python is no exception, and today we’re bringing you a unit-testing Python tutorial. We’ll start the post with a brief definition of unit testing in Python, featuring a simple example. From there, we’ll dive into the tutorial itself. You’ll learn how to write and run unit tests in Python, including which tools to use and what best practices to follow. As I write this post, I’m using Python version 3.11.0 and Visual Studio Code as my text editor.
What is unit testing in Python? With an example
Unit testing in Python doesn’t differ dramatically from unit testing in other languages. So, let’s take a step back and define unit testing more generally. Unit testing is writing automated tests that exercise small portions of the codebase—the so-called units—to verify whether they work as intended. A crucial characteristic of unit tests is that they don’t interact with external dependencies—which are slow and often unreliable—but substitute them with fake implementations, called stubs, test doubles, or (the most popular term) mocks. Here is the famous unit test definition by Michael Feathers:
A test is not a unit test if:
- It talks to the database
- It communicates across the network
- It touches the file system
- It can’t run at the same time as any of your other unit tests
- You have to do special things to your environment (such as editing config files) to run it.
The properties of unit tests bring some significant benefits:
- Since they don’t talk to slow external dependencies, unit tests run rapidly.
- A good unit test only fails due to a change in the code, leading to tests that aren’t brittle.
- Specific feedback. Since a unit test exercises a single unit, its feedback is localized. If a unit test fails, you often know immediately where the problem is.
The anatomy of a unit test
What does a unit test look like? You’ll soon see several examples, but let’s first talk about the general anatomy of a test. A unit test usually starts with some preparation, in which you create and perhaps configure the object you’re testing (the system under test, or SUT). Then, you perform some action. Finally, you verify whether the result was what you expected. This is often called the arrange-act-assert pattern of unit tests. [CTA heading=”Expand Your Test Coverage” content=”Fast and flexible authoring of AI-powered end-to-end tests — built for scale.” button_text=”Start Testing Free” button_url=”https://bit.ly/386Q3KG”]
Example
Let’s see a quick example. Consider the following excerpt of code:
def add(numbers):
if numbers is None or numbers == "":
return 0
numbers = [int(x) for x in numbers.split(",")]
negatives = [x for x in numbers if x < 0]
if negatives:
raise ValueError("Negatives not allowed: " + ",".join(str(x) for x in negatives))
return sum(x for x in numbers if x <= 1000)
The function above is a possible solution for a programming exercise called “the string calculator kata,” created by Roy Osherove. The idea is to create a function called add, which gets a string as an argument containing integers separated by a comma. The function then calculates and returns the sum of the numbers. But there are some additional rules:
- An empty string should result in zero
- Numbers larger than 1000 should be ignored
- Negative numbers aren’t allowed; the function should throw an exception with a message listing all negatives provided
There are more rules, but the ones above are enough for us to start. The exercise rules also state that we shouldn’t care about input validation but simply assume that input will always be correct. I ignored that a little bit and added an extra guard clause that returns zero when the input is null (None, as it’s called in Python.) Now, let’s see a few examples of tests that exercise the method above:
class TestAddFunction(unittest.TestCase):
def test_add_empty_string(self):
self.assertEqual(add(""), 0)
def test_add_none(self):
self.assertEqual(add(None), 0)
def test_add_single_number(self):
self.assertEqual(add("5"), 5)
def test_add_multiple_numbers(self):
self.assertEqual(add("1,2,3,4,5"), 15)
def test_add_ignores_numbers_larger_than_1000(self):
self.assertEqual(add("1,1001,2,3"), 6)
def test_add_raises_error_for_negatives(self):
with self.assertRaises(ValueError) as context:
add("1,-2,3,-4")
self.assertEqual(str(context.exception), "Negatives not allowed: -2,-4")
Here we have a test class with several test methods that verify the following scenarios:
- empty string results in zero
- None results in zero
- a single number results in itself
- several numbers result in their sum
- a number larger than 1000 isn’t considered
- negative numbers cause the function to throw, and the numbers themselves are listed in the exception message
In case you’re wondering why the tests above don’t follow the structure laid out before, here is one of the tests, rewritten to match the AAA pattern:
def test_add_multiple_numbers(self):
# arrange
expected = 15
# act
actual = add("1,2,3,4,5")
# assert
self.assertEqual(actual, expected)
Here’s your unit testing Python tutorial: Time to get started
Let’s now learn how to unit test in Python, step by step. First, we need some code to test.
Create the production code
Using whatever editor you like the most, create a file called average.py with the following content:
def average(numbers):
if not numbers:
return 0
numbers.append(len(numbers))
return sum(numbers) / len(numbers)
if __name__ == '__main__':
print('Enter numbers separated by space:')
numbers = [int(x) for x in input().split()]
print(average(numbers))
This is the strangest, silliest function I could think of quickly, so let’s use it as an example. The idea here is simple: we’ll pass a list to the method. If it’s empty or None—or otherwise falsy—the method will return zero. Otherwise, it will add an additional item to the list whose value is the length of the list. Then, it calculates and returns the average of the items on the list.
Run the code with python average.py, and you’ll see the script working.
Preparing the terrain for testing
With our production code in place, let’s get what we need to start unit testing. If you don’t have virtualenv installed, run this:
pip install virtualenv
Then, create the virtual environment for your project:
virtualenv .venv
Finally, navigate to the Scripts folder inside the .venv folder:
cd .venv/Scripts
And activate the virtual environment using the appropriate script. In my case, I’m on Windows, so I’ll use the Powershell one:
.\activate.ps1
Navigate back to the root of your project folder. Create a file called requirements.txt with the following line:
pytest==7.2.1
Then, install it with pip install -r requirements.txt. After that, you’re ready to start testing!
Writing your first test
Inside your project folder, create a new file called test_average.py. Warning: the name is essential. If the file doesn’t start with the word “test,” pytest won’t be able to run the tests. Then, paste the following content inside the file:
import pytest
from average import average
class TestAverage:
def test_average_empty_list(self):
list = []
expected = 0
actual = average(list)
assert expected == actual
The file starts by importing both pytest itself and our code file. Then, it defines the test class and one test method. The test method passes an empty string to the average() function and asserts (that is, expresses an expectation) that the result should be zero. Run the test by running pytest in the root of your project. The test should pass, and you’ll see a result like the following: 
More tests
Add more tests to cover more scenarios:
def test_average_none(self):
list = None
expected = 0
actual = average(list)
assert expected == actual
def test_average_list_with_single_number(self):
list = [1]
expected = 1
actual = average(list)
assert expected == actual
def test_average_list_with_two_numbers(self):
list = [1, 3]
expected = 2
actual = average(list)
assert expected == actual
The tests above cover the following scenarios:
- passing None should result in zero
- passing a list with a single number should result in the number itself
- a list with one and three should result in two
To understand the last scenario, remember that since the list has two items initially, the number 2 is appended to it as the last element. 1 plus 3 plus 2 equals six, which divided by three is 2.
Making the tests fail
When writing tests, a great practice is to see the tests failing. That way, you ensure your tests are (probably) right and haven’t succeeded by coincidence. TDD (test-driven development) is a practice that offers “see the test failing” as a built-in feature since you start by writing a failing test. Anyway, ensure you see the tests failing when they should be failing. This can be as simple as changing something in the production code so it becomes incorrect and rerunning the tests. For instance, let’s change the return statement from the average() function so it returns a hardcoded number:
def average(numbers):
if not numbers:
return 0
numbers.append(len(numbers))
# return sum(numbers) / len(numbers)
return 123
Now, if I run the tests, the two last ones (the ones that use valid, non-empty lists) fail: Don’t forget to change the function back again.

Don’t forget to change the function back again.
How do you write a good unit test in Python?
You’ve just seen how to start unit testing your Python code. But how can you ensure the tests you write are any good?
Python unit testing best practices
There are some general “rules” you should follow when testing your code, Python or otherwise:
- Test methods shouldn’t depend on other test methods. You should be able to run them on any order or alone.
- Don’t put logic (if statements, for statements) into your tests; keep their cyclomatic complexity low so they’re easy to read and understand.
- Make sure they’re part of the CI/CD pipeline to catch defects before the code reaches production.
- Do what you can to ensure your tests are fast. Otherwise, developers will run them less often.
Unittest vs. pytest
Choosing a unit testing framework is important if you want to write good unit tests. In this post, we’ve used pytest, except in the first example, in which we used unittest. How do they compare? In short: unittest is the standard unit testing framework for Python. It comes with the language itself, and it’s easy to start. pytest is a third-party project; as you’ve seen, you need to install it, but it’s also quite easy to use. So, what’s the verdict? Choosing between both frameworks is mainly a matter of preference. Some argue that unittest tests are more readable since they use assertion methods, while pytest does not. On the other hand, others might find that the output from pytest is more comprehensive. At the end of the day, experiment with both and choose the one you like the most.
Happy testing!
Python is one of the most popular programming languages out there. It’s not only powerful and flexible but also approachable, making it an excellent choice for both seasoned professionals and newcomers to software development. In this post, you’ve seen how to get started with Python unit testing. You also learned some fundamentals and best practices about unit testing, the benefits of adopting it, and the two main frameworks for Python. It’s time to get out there and start testing!
