By WebNest Studio Engineering Team
Python Tutorial
Testing Python Code with unittest and pytest
Automated tests are code that verifies your other code behaves correctly, run repeatedly and automatically rather than checked by hand every time. Python ships with a built-in testing framework called unittest, and the third-party pytest package has become the de facto standard for most modern Python projects because of its simpler syntax and richer features.
Writing tests catches regressions early — when a change to one part of a codebase accidentally breaks another part — and serves as executable documentation of how a function is actually supposed to behave.
Why it matters
Code without tests can only be checked by running it by hand, and every change risks breaking something that used to work. Automated tests check it in seconds, every time. Writing tests is a standard part of professional development: most teams will not accept a change without them, and many interviews include a testing question.
assert Statements — The Foundation of Testing
At its core, a test checks that some condition is true, and Python's built-in assert statement does exactly that: assert condition, "optional message" does nothing if the condition is true, and raises an AssertionError (with the optional message) if it's false. Both unittest and pytest build on this same fundamental idea, just with more structure and better failure reporting around it.
Writing Tests with unittest
unittest is class-based: you subclass unittest.TestCase and write methods whose names start with test_. Instead of plain assert, TestCase provides methods like self.assertEqual(a, b), self.assertTrue(x), and self.assertRaises(ExceptionType), which produce clearer failure messages than a bare assert would. Running python -m unittest from a project's root automatically discovers and runs any file matching the pattern test_*.py containing TestCase subclasses.
Writing Tests with pytest
pytest lets you write tests as plain functions (no class or special base class required) using ordinary assert statements — it rewrites assertions internally to produce detailed, readable failure output showing exactly which values didn't match. Like unittest, it automatically discovers test files (again, files and functions named test_* or *_test by default) and runs them when you invoke the pytest command in a project directory, without needing to import or register each test file manually.
Example
The function under test and equivalent unittest and pytest tests
# file: calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# file: test_calculator_unittest.py
import unittest
from calculator import add, divide
class TestCalculator(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
def test_divide_by_zero_raises(self):
with self.assertRaises(ValueError):
divide(10, 0)
if __name__ == "__main__":
unittest.main()
# file: test_calculator_pytest.py
from calculator import add, divide
import pytest
def test_add():
assert add(2, 3) == 5
def test_divide_by_zero_raises():
with pytest.raises(ValueError):
divide(10, 0)
# Run with: python -m unittest (discovers test_calculator_unittest.py)
# Run with: pytest (discovers test_calculator_pytest.py)
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
================= 2 passed in 0.01s =================
Common Mistakes
- Naming test files or functions without the "test_" prefix (or "_test" suffix), causing automatic test discovery to silently skip them.
- Using a bare assert with no message inside a large test, making it hard to tell which specific check failed when a test fails.
- Writing tests that depend on execution order or on state left behind by a previous test, causing tests to pass or fail unpredictably depending on how they are run.
- Testing only the "happy path" and never checking that invalid input correctly raises the expected exception.
Exercise
Write slugify, which turns a title into a lower-case string with hyphens in place of spaces, and two test functions for it in the pytest style, using assert. With pytest installed, the tests would be run with the pytest command; here they are called directly, and a message is printed if both pass.
Expected output: 2 passed
def slugify(title):
# TODO: strip surrounding spaces, convert to lower case, replace spaces with hyphens
pass
def test_replaces_spaces():
# TODO: assert that "Hello World" becomes "hello-world"
pass
def test_strips_outer_spaces():
# TODO: assert that " Python " becomes "python"
pass
test_replaces_spaces()
test_strips_outer_spaces()
print("2 passed")
Show hints
- Chain the string methods:
title.strip().lower().replace(" ", "-"). - A test is a plain
assert actual == expected. A failing assert raisesAssertionErrorand stops the program before the message is printed.
Show solution
def slugify(title):
return title.strip().lower().replace(" ", "-")
def test_replaces_spaces():
assert slugify("Hello World") == "hello-world"
def test_strips_outer_spaces():
assert slugify(" Python ") == "python"
test_replaces_spaces()
test_strips_outer_spaces()
print("2 passed")
Quick quiz
1. How does pytest discover test functions?
- A. They must be registered in a list
- B. By name: files named test_*.py and functions whose names start with test_
- C. They must inherit from a class
- D. By a special comment
Show answer
B. No registration is needed; following the naming convention is enough.
2. In unittest, which class do test classes inherit from?
- A. unittest.Test
- B. unittest.Suite
- C. unittest.TestCase
- D. object
Show answer
C. It provides the assert methods such as assertEqual.
3. What should a good unit test do?
- A. Test the whole application at once
- B. Print its results for a person to read
- C. Connect to the production database
- D. Check one behaviour, run quickly and not depend on other tests
Show answer
D. Independent tests can be run in any order and point directly at what is broken.
Interview questions
What is the difference between unittest and pytest?
Show answer
unittest is in the standard library and follows the class-based style of JUnit: tests are methods of a TestCase subclass and use methods such as assertEqual. pytest is a third-party tool in which tests are plain functions using the ordinary assert statement, with detailed failure messages, fixtures for set-up and parametrisation for running one test with many inputs. pytest can also run unittest tests and is the more common choice.
What is a fixture?
Show answer
A fixture provides something a test needs, such as a temporary file, a database connection or a prepared object, and cleans it up afterwards. In pytest it is a function decorated with @pytest.fixture, and a test receives it by naming it as a parameter. Fixtures keep set-up code out of the tests themselves and let it be shared.
Key Points to Remember
- assert is the underlying mechanism both unittest and pytest build on: it raises AssertionError when a condition is false.
- unittest is class-based (subclass TestCase, methods named test_*) and ships in the standard library.
- pytest allows plain functions with ordinary assert statements and produces detailed failure diffs automatically.
- Both frameworks auto-discover test files and functions following a test_*/*_test naming convention.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs Python in your browser. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering