
Chapter Outline
Chapter 7: Testing Your Code
Testing ensures your programs work as expected and continue working as you add new features. In professional software development, writing tests is not optional—it’s a best practice.
In this chapter, we’ll cover:
- Basics of unit testing in Python using
unittestandpytest. - Using fixtures, mocks, and parameterized tests to write cleaner tests.
- The Test-Driven Development (TDD) mindset.
By the end, you’ll know how to structure, run, and maintain tests for your Python applications.
7.1 Unit Testing with unittest
Python includes a built-in testing framework called unittest.
Example: testing a simple calculator.
calculator.py1def add(a: int, b: int) -> int:2 """Return the sum of two numbers."""3 return a + b
Now a unittest test case:
test_calculator_unittest.py1import unittest2from calculator import add34class TestCalculator(unittest.TestCase):5 def test_add(self):6 # Arrange: set up inputs7 a, b = 2, 38 # Act: call the function9 result = add(a, b)10 # Assert: verify result11 self.assertEqual(result, 5)1213if __name__ == "__main__":14 unittest.main()
unittest.TestCaseis the base class for all test cases.- Each method starting with
test_is run as a separate test. - We used the AAA pattern: Arrange → Act → Assert.
- Running
python test_calculator_unittest.pyexecutes the test.
7.2 Testing with pytest
pytest is a third-party testing framework that is more concise and powerful.
test_calculator_pytest.py1from calculator import add23def test_add():4 # Arrange5 a, b = 2, 36 # Act7 result = add(a, b)8 # Assert9 assert result == 5
- No class or inheritance required—just functions prefixed with
test_. assertis used for comparisons.- Run with
pytestcommand in the terminal; pytest finds tests automatically.
7.3 Fixtures in pytest
Fixtures help you set up and tear down test resources.
test_contacts.py1import pytest23@pytest.fixture4def sample_contacts():5 """Provide a sample contact dictionary before each test."""6 return {"Alice": {"phone": "123", "email": "alice@example.com"}}78def test_contact_lookup(sample_contacts):9 # Act10 contact = sample_contacts.get("Alice")11 # Assert12 assert contact["phone"] == "123"
@pytest.fixturemarks a function as a fixture.- Any test function that accepts the fixture as a parameter automatically receives the fixture’s return value.
- Fixtures reduce duplication and make tests cleaner.
7.4 Mocks
Sometimes you want to replace real dependencies with fake ones. Example: testing a function that sends email.
notifier.py1def send_email(address: str, subject: str) -> str:2 """Pretend to send an email (in real apps this would call an API)."""3 return f"Email sent to {address} with subject '{subject}'"
Now, mock it in tests:
test_notifier.py1from unittest.mock import patch2from notifier import send_email34def test_send_email_mocked():5 with patch("notifier.send_email", return_value="Mocked!") as mock_send:6 result = send_email("bob@example.com", "Hello")7 assert result == "Mocked!"8 mock_send.assert_called_once_with("bob@example.com", "Hello")
patchreplacessend_emailtemporarily with a mock function.- The test doesn’t actually send an email, but it checks that the right call was made.
7.5 Parameterized Tests
Instead of writing multiple test functions, you can parametrize one test with different inputs.
test_math.py1import pytest2from calculator import add34@pytest.mark.parametrize(5 "a, b, expected",6 [7 (1, 2, 3), # case 18 (0, 0, 0), # case 29 (-1, 1, 0), # case 310 ],11)12def test_add_parametrized(a, b, expected):13 assert add(a, b) == expected
@pytest.mark.parametrizeruns the same test with multiple input sets.- This reduces boilerplate and ensures you test edge cases.
7.6 Test-Driven Development (TDD) Mindset
TDD is a workflow where you:
- Write a failing test for new functionality.
- Implement the minimal code to make the test pass.
- Refactor the code and tests to improve quality.
Example workflow:
- Write
test_calculator.pywithtest_subtract(). - Run tests → it fails (function doesn’t exist).
- Implement
subtract()incalculator.py. - Run tests again → they pass.
- Refactor if needed.
This ensures tests guide your design and prevent regression bugs.
Summary
unittestis the built-in framework, whilepytestis the community favorite for power and simplicity.- Fixtures, mocks, and parameterized tests let you write robust and maintainable test suites.
- TDD encourages writing tests before code, improving design and reliability.
Next, we’ll explore Object-Oriented Programming (OOP), where tests will continue to play a critical role.
Check your understanding
Test your knowledge of Testing in Python