Introduction to Testing Your Code
Welcome back, future Pythonista! So far, you’ve learned to write amazing Python code, build functions, create classes, and even handle errors. But how do you know your code actually works as intended, especially as it grows more complex? How do you ensure that adding a new feature doesn’t accidentally break an old one?
The answer, my friend, is testing! In this chapter, we’re going to dive into the incredibly important world of unit testing in Python. You’ll learn how to write small, focused tests for individual pieces of your code, giving you confidence that your programs are robust and reliable. We’ll explore Python’s built-in testing framework, unittest, and then introduce you to pytest, a hugely popular and powerful third-party testing tool that many developers prefer.
By the end of this chapter, you’ll not only understand why testing is crucial but also have the practical skills to implement tests for your own Python projects. You should be comfortable with basic Python syntax, functions, and modules from our previous chapters. Let’s make your code bulletproof!
Core Concepts: Why Test and What to Test?
Before we jump into writing actual tests, let’s understand the fundamental ideas behind software testing.
Why Do We Test Our Code?
Imagine building a complex machine without ever checking if its individual gears and levers work correctly. You wouldn’t know if it’s going to achieve its purpose or fall apart at the first push of a button! Software is no different.
Here’s why testing is absolutely essential:
- Catch Bugs Early: The earlier you find a bug, the cheaper and easier it is to fix. Tests act as an early warning system.
- Ensure Correctness: Tests verify that your code behaves exactly as you expect under various conditions.
- Facilitate Refactoring: When you want to improve or restructure your code (refactor), a solid suite of tests gives you the confidence that you haven’t introduced regressions (new bugs in old functionality).
- Improve Design: Writing tests often forces you to think more clearly about your code’s design, leading to more modular and testable components.
- Documentation: Well-written tests can serve as living documentation, showing how different parts of your code are supposed to be used.
What is a “Unit Test”?
In this chapter, we’re focusing on unit tests. A unit test is a small, isolated test that verifies a single “unit” of code. What’s a “unit”? It’s typically:
- A single function
- A single method within a class
- A small module
The goal is to test these units in isolation, meaning they shouldn’t depend on external factors like databases, network requests, or user input. If your add function works, you want to be sure it always works, regardless of what else your program is doing.
Test-Driven Development (TDD) - A Quick Peek
You might hear about Test-Driven Development (TDD). It’s a development methodology where you write your tests before you write the actual code. The cycle goes like this:
- Red: Write a test that fails (because the feature doesn’t exist yet).
- Green: Write just enough code to make the test pass.
- Refactor: Clean up your code, knowing your tests will catch any accidental breakage.
While we won’t strictly follow TDD today, understanding its existence highlights the importance of tests in the development lifecycle.
Introducing unittest: Python’s Built-in Framework
Python comes with a built-in module called unittest that provides a rich set of tools for creating unit tests. It’s inspired by JUnit, a popular testing framework for Java.
Key features of unittest:
- Test Cases: Tests are organized into classes that inherit from
unittest.TestCase. - Assertion Methods:
unittestprovides manyassertmethods (likeassertEqual,assertTrue,assertRaises) to check conditions. - Test Discovery: It can automatically find and run your tests.
Meeting pytest: The Popular Alternative
While unittest is perfectly capable, pytest has become the de facto standard for testing in the Python community due to its simplicity, power, and extensibility.
Key advantages of pytest:
- Simpler Assertions: You use standard Python
assertstatements, making tests more readable. - No Boilerplate: You don’t need to inherit from a
TestCaseclass for basic tests. Test functions are just regular functions. - Powerful Features: It offers advanced features like fixtures (for setting up test environments), parameterization (running the same test with different inputs), and plugins.
We’ll start with unittest to understand the fundamentals, then move to pytest to see why it’s so beloved.
Step-by-Step Implementation: Testing a Simple Calculator
Let’s get our hands dirty! We’ll create a very simple calculator module and then write tests for its functions using both unittest and pytest.
Step 1: Set Up Your Project
First, create a new directory for this chapter’s code. Let’s call it testing_project.
mkdir testing_project
cd testing_project
Inside this directory, create a new Python file named calculator.py. This will hold the code we want to test.
Step 2: Write the Code to Be Tested (calculator.py)
Open calculator.py and add the following simple function:
# testing_project/calculator.py
def add(a, b):
"""Adds two numbers and returns the result."""
return a + b
That’s it for our calculator.py for now. Simple, right? But even simple functions can have subtle bugs, or we might want to ensure they handle various inputs correctly.
Step 3: Writing Tests with unittest
Now, let’s create our first test file using unittest. In the same testing_project directory, create a new file named test_calculator_unittest.py.
We’ll start by importing the necessary components.
# testing_project/test_calculator_unittest.py
import unittest
from calculator import add
Here, we import the unittest module itself, and we also import our add function from the calculator.py file we just created.
Next, we define our first test class. In unittest, test cases are organized into classes that inherit from unittest.TestCase.
# testing_project/test_calculator_unittest.py
import unittest
from calculator import add
class TestCalculator(unittest.TestCase):
# Tests will go here
pass
Notice TestCalculator(unittest.TestCase). This tells Python that our TestCalculator class is a special kind of class designed to hold tests. The pass is a placeholder for now.
Now, let’s add our first test method inside the TestCalculator class. Test methods in unittest must start with the word test_. This is how unittest knows which methods to run as tests.
# testing_project/test_calculator_unittest.py
import unittest
from calculator import add
class TestCalculator(unittest.TestCase):
def test_add_positive_numbers(self):
"""Test that the add function correctly sums two positive numbers."""
result = add(10, 5)
self.assertEqual(result, 15)
Let’s break down this small addition:
def test_add_positive_numbers(self):: This is our test method. It takesselfas its first argument, just like any other method in a class. The nametest_add_positive_numbersis descriptive and tells us what this test is verifying.result = add(10, 5): We call ouraddfunction with some sample inputs (10 and 5).self.assertEqual(result, 15): This is an assertion.unittest.TestCaseprovides many assertion methods.assertEqualchecks if the first argument is equal to the second argument. Ifresultis not equal to15, this test will fail, andunittestwill report it. If they are equal, the test passes.
Running unittest Tests
To run this test, open your terminal (make sure you’re in the testing_project directory) and type:
python -m unittest test_calculator_unittest.py
You should see output similar to this:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
The . indicates that one test passed. OK confirms everything is good!
Let’s add another test to our TestCalculator class, this time checking for negative numbers.
# testing_project/test_calculator_unittest.py
import unittest
from calculator import add
class TestCalculator(unittest.TestCase):
def test_add_positive_numbers(self):
"""Test that the add function correctly sums two positive numbers."""
result = add(10, 5)
self.assertEqual(result, 15)
def test_add_negative_numbers(self):
"""Test that the add function handles negative numbers correctly."""
result = add(-10, -5)
self.assertEqual(result, -15)
def test_add_mixed_numbers(self):
"""Test that the add function handles a mix of positive and negative numbers."""
result = add(10, -5)
self.assertEqual(result, 5)
Run the tests again:
python -m unittest test_calculator_unittest.py
You should now see three dots, indicating three passing tests:
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
unittest Test Discovery
Instead of specifying the file name, unittest can also discover tests in your current directory and subdirectories. If your test files are named following a pattern (like test_*.py), you can just run:
python -m unittest discover
This is very useful for larger projects!
Step 4: Writing Tests with pytest
Now, let’s explore pytest. First, you’ll need to install it. As of December 2025, the latest stable pytest version is likely to be 8.x.x or 9.x.x. We’ll use a general recommendation here, but always check the official pytest documentation for the absolute latest.
pip install pytest
If you don’t have pip or Python installed, please refer to Chapter 1 for setup instructions. Remember, we’re using Python 3.14.1 as the latest stable version as of 2025-12-03. You can download it from python.org/downloads/release/python-3141/.
Now, create a new file named test_calculator_pytest.py in your testing_project directory.
We’ll start by importing our add function:
# testing_project/test_calculator_pytest.py
from calculator import add
With pytest, you don’t need a special class or to inherit from unittest.TestCase for basic tests. You just write regular Python functions whose names start with test_.
# testing_project/test_calculator_pytest.py
from calculator import add
def test_add_positive_numbers_pytest():
"""Test that the add function correctly sums two positive numbers using pytest."""
assert add(10, 5) == 15
Notice the difference? Instead of self.assertEqual(result, 15), we simply use a standard Python assert statement: assert add(10, 5) == 15. pytest cleverly intercepts these assertions and provides detailed failure information if they don’t pass. This makes tests much more concise and readable.
Running pytest Tests
To run pytest tests, simply navigate to your testing_project directory in the terminal and type:
pytest
pytest automatically discovers test files (files starting with test_ or ending with _test.py) and test functions/methods within them.
You should see output like this:
============================= test session starts ==============================
platform linux -- Python 3.14.1, pytest-8.x.x, pluggy-1.x.x
rootdir: /path/to/your/testing_project
collected 1 item
test_calculator_pytest.py . [100%]
============================== 1 passed in 0.01s ===============================
The . again indicates a passing test.
Let’s add a few more pytest tests to cover different scenarios for our add function:
# testing_project/test_calculator_pytest.py
from calculator import add
def test_add_positive_numbers_pytest():
"""Test that the add function correctly sums two positive numbers using pytest."""
assert add(10, 5) == 15
def test_add_negative_numbers_pytest():
"""Test that the add function handles negative numbers correctly using pytest."""
assert add(-10, -5) == -15
def test_add_mixed_numbers_pytest():
"""Test that the add function handles a mix of positive and negative numbers using pytest."""
assert add(10, -5) == 5
def test_add_zero_pytest():
"""Test that the add function handles zero correctly using pytest."""
assert add(0, 5) == 5
assert add(5, 0) == 5
assert add(0, 0) == 0
Run pytest again:
pytest
You’ll see all four tests pass (four dots). Notice how we put multiple assertions in test_add_zero_pytest. While generally it’s good practice to have one assertion per test (or one logical assertion), pytest handles multiple assertions gracefully.
Quick Comparison: unittest vs. pytest
| Feature | unittest | pytest |
|---|---|---|
| Test Structure | Classes inheriting from unittest.TestCase | Functions prefixed with test_ |
| Assertions | self.assertEqual(), self.assertTrue(), etc. | Standard Python assert statements |
| Setup/Teardown | setUp(), tearDown() methods | Fixtures (more powerful and flexible) |
| Readability | Can be verbose | Generally more concise and readable |
| Extensibility | Good, but pytest has a rich plugin ecosystem | Excellent, with many community plugins |
For most new Python projects, pytest is the recommended choice due to its simplicity and flexibility. However, unittest is built-in and perfectly functional, especially for smaller projects or if you prefer its xUnit-style structure.
Mini-Challenge: Extend the Calculator and Test It!
Alright, it’s your turn to put your new testing superpowers to work!
Challenge:
- Add a
subtractfunction to yourcalculator.pyfile. This function should take two arguments,aandb, and returna - b. - Write tests for the
subtractfunction using bothunittest(intest_calculator_unittest.py) andpytest(intest_calculator_pytest.py). - Ensure your tests cover different scenarios:
- Subtracting a smaller positive number from a larger positive number.
- Subtracting a larger positive number from a smaller positive number (resulting in a negative).
- Subtracting a negative number.
- Subtracting zero.
- Run all your tests (
python -m unittest discoverandpytest) to ensure everything passes!
Hint:
For unittest, remember to add new methods (e.g., test_subtract_positive, test_subtract_negative) to your TestCalculator class. For pytest, just create new functions (e.g., test_subtract_positive_pytest, test_subtract_zero_pytest) in your test_calculator_pytest.py file. Use self.assertEqual() for unittest and plain assert for pytest.
What to Observe/Learn:
- How easy it is to extend your test suite as you add new functionality.
- The slight differences in syntax between
unittestandpytestfor similar test cases. - The confidence you gain when all your tests pass after adding new code!
Take your time, try to solve it independently, and remember to have fun with it!
Need a little nudge? Click for a hint!
Don't forget to import your new `subtract` function into both test files!
Ready for the solution? Click to reveal!
First, update calculator.py:
# testing_project/calculator.py
def add(a, b):
"""Adds two numbers and returns the result."""
return a + b
def subtract(a, b):
"""Subtracts the second number from the first and returns the result."""
return a - b
Next, update test_calculator_unittest.py:
# testing_project/test_calculator_unittest.py
import unittest
from calculator import add, subtract # Don't forget to import subtract!
class TestCalculator(unittest.TestCase):
def test_add_positive_numbers(self):
"""Test that the add function correctly sums two positive numbers."""
result = add(10, 5)
self.assertEqual(result, 15)
def test_add_negative_numbers(self):
"""Test that the add function handles negative numbers correctly."""
result = add(-10, -5)
self.assertEqual(result, -15)
def test_add_mixed_numbers(self):
"""Test that the add function handles a mix of positive and negative numbers."""
result = add(10, -5)
self.assertEqual(result, 5)
# New tests for subtract function
def test_subtract_positive_numbers(self):
"""Test that the subtract function correctly handles positive numbers."""
self.assertEqual(subtract(10, 5), 5)
self.assertEqual(subtract(5, 10), -5)
def test_subtract_negative_numbers(self):
"""Test that the subtract function handles negative numbers correctly."""
self.assertEqual(subtract(-10, -5), -5)
self.assertEqual(subtract(-5, -10), 5)
def test_subtract_mixed_numbers(self):
"""Test that the subtract function handles a mix of positive and negative numbers."""
self.assertEqual(subtract(10, -5), 15)
self.assertEqual(subtract(-10, 5), -15)
def test_subtract_zero(self):
"""Test that the subtract function handles zero correctly."""
self.assertEqual(subtract(5, 0), 5)
self.assertEqual(subtract(0, 5), -5)
self.assertEqual(subtract(0, 0), 0)
And finally, update test_calculator_pytest.py:
# testing_project/test_calculator_pytest.py
from calculator import add, subtract # Don't forget to import subtract!
def test_add_positive_numbers_pytest():
"""Test that the add function correctly sums two positive numbers using pytest."""
assert add(10, 5) == 15
def test_add_negative_numbers_pytest():
"""Test that the add function handles negative numbers correctly using pytest."""
assert add(-10, -5) == -15
def test_add_mixed_numbers_pytest():
"""Test that the add function handles a mix of positive and negative numbers using pytest."""
assert add(10, -5) == 5
def test_add_zero_pytest():
"""Test that the add function handles zero correctly using pytest."""
assert add(0, 5) == 5
assert add(5, 0) == 5
assert add(0, 0) == 0
# New tests for subtract function
def test_subtract_positive_numbers_pytest():
"""Test that the subtract function correctly handles positive numbers using pytest."""
assert subtract(10, 5) == 5
assert subtract(5, 10) == -5
def test_subtract_negative_numbers_pytest():
"""Test that the subtract function handles negative numbers correctly using pytest."""
assert subtract(-10, -5) == -5
assert subtract(-5, -10) == 5
def test_subtract_mixed_numbers_pytest():
"""Test that the subtract function handles a mix of positive and negative numbers using pytest."""
assert subtract(10, -5) == 15
assert subtract(-10, 5) == -15
def test_subtract_zero_pytest():
"""Test that the subtract function handles zero correctly using pytest."""
assert subtract(5, 0) == 5
assert subtract(0, 5) == -5
assert subtract(0, 0) == 0
Now run your tests!
python -m unittest discover
pytest
You should see all your tests passing! Great job!
Common Pitfalls & Troubleshooting
Even with simple testing, you might run into a few common issues. Here are some to watch out for:
Forgetting
selfinunittestmethods:- Pitfall: In
unittest.TestCasemethods, you must includeselfas the first argument (e.g.,def test_something(self):). Forgetting it will lead to aTypeError. - Fix: Always define your
unittesttest methods withself. - Example Error:
TypeError: test_add_positive_numbers() takes 0 positional arguments but 1 was given
- Pitfall: In
Incorrect Imports:
- Pitfall: If you get a
ModuleNotFoundErrororImportErrorwhen running your tests, it means Python can’t find the module or function you’re trying to import. - Fix: Double-check your
from <module> import <function>statements. Ensure your test files are in the correct directory relative to the code they’re testing, or that your project structure is set up for Python to find modules (e.g., by using a virtual environment andpip install -e .for more complex packages, which we’ll cover in later chapters). For now, keepingcalculator.pyand your test files in the same directory works well.
- Pitfall: If you get a
Not Running Tests from the Correct Directory:
- Pitfall:
unittest discoverandpytestrely on finding test files. If you run them from a different directory than yourtesting_project, they might not find anything. - Fix: Always
cdinto the root of your project directory (e.g.,testing_project) before runningpython -m unittest discoverorpytest.
- Pitfall:
Over-testing vs. Under-testing:
- Pitfall: It’s a balance. Under-testing means you don’t have enough tests to catch bugs. Over-testing means you’re writing tests for every trivial detail, making your test suite brittle and hard to maintain.
- Best Practice: Focus on testing the public interface of your functions/methods. Test edge cases, invalid inputs, and typical scenarios. Don’t worry about testing Python’s built-in functions or simple getters/setters unless they contain complex logic. The goal is to get good “test coverage” – ensuring your tests exercise most of your code’s paths. Tools exist to measure test coverage, which you might explore later!
Summary
Phew! You’ve just taken a huge leap in becoming a more professional and confident Python developer. Let’s recap what we’ve learned:
- Why Test: Testing is crucial for catching bugs early, ensuring code correctness, facilitating safe refactoring, improving design, and serving as living documentation.
- Unit Tests: We focus on unit tests, which verify small, isolated “units” of code like functions or methods.
unittest: Python’s built-in testing framework.- Tests are organized into classes inheriting from
unittest.TestCase. - Test methods must start with
test_. - Uses assertion methods like
self.assertEqual(). - Run with
python -m unittest discoverorpython -m unittest <test_file.py>.
- Tests are organized into classes inheriting from
pytest: A popular and powerful third-party testing framework.- Tests are typically simple functions starting with
test_. - Uses standard Python
assertstatements. - Easier to write and often more readable than
unittest. - Run with
pytest.
- Tests are typically simple functions starting with
- Practical Application: You’ve built a simple
calculatormodule and written unit tests for itsaddandsubtractfunctions using bothunittestandpytest. - Common Pitfalls: We discussed issues like forgetting
self, import errors, and running tests from the wrong directory.
You now have a solid foundation for writing effective unit tests in Python. This skill will pay dividends as you build more complex applications.
What’s Next?
In the upcoming chapters, we’ll continue to build on these advanced Python concepts. We might explore:
- Chapter 20: Handling External Dependencies with Mocking: Learn how to isolate your tests from external systems like databases or web services using
unittest.mockorpytest-mock. - Chapter 21: Packaging Your Python Project: Understand how to structure your project, manage dependencies, and create installable packages for sharing your code.
Keep practicing, keep coding, and keep testing! Your journey to Python mastery continues!