Python Unit Testing with unittest
Learn how to write, organize, discover, and troubleshoot Python unit tests with unittest, including fixtures, assertions, imports, and pytest compatibility.

Direct answer: create a class that inherits from unittest.TestCase, add methods whose names start with test, use assertion methods to compare expected and actual values, then run python -m unittest. Python’s unittest documentation describes the framework in terms of test cases, fixtures, suites, and a test runner.
1. A minimal unittest test
Suppose the application code is in calculator.py:
def add(left, right):
return left + right
def divide(left, right):
if right == 0:
raise ValueError("right must not be zero")
return left / right
Create test_calculator.py beside it:
import unittest
from calculator import add, divide
class CalculatorTests(unittest.TestCase):
def test_add_returns_the_sum(self):
self.assertEqual(add(2, 3), 5)
def test_divide_returns_the_quotient(self):
self.assertEqual(divide(8, 2), 4)
def test_divide_rejects_zero(self):
with self.assertRaises(ValueError):
divide(8, 0)
if __name__ == "__main__":
unittest.main()
Each method is self-contained and can run independently. The test prefix is the default naming convention used by discovery. Assertions should describe the behavior your code promises, rather than the implementation details that happen to produce it.
2. Run the tests
Run the file directly
python test_calculator.py
Run unittest discovery
python -m unittest
The module command starts the default discovery process. You can also invoke discovery explicitly:
python -m unittest discover
For a readable progress stream, add verbosity:
python -m unittest -v
Run one module, class, or method
python -m unittest test_calculator
python -m unittest test_calculator.CalculatorTests
python -m unittest test_calculator.CalculatorTests.test_add_returns_the_sum
Use a dotted import path, not a filesystem path, when selecting a particular test object.
3. How test discovery works
Discovery finds test files, imports them, and loads test cases from the imported modules. The default filename pattern is test*.py. You can control the search with:

| Option | Meaning | Example |
|---|---|---|
-s |
Start directory | python -m unittest discover -s tests |
-p |
Filename pattern | python -m unittest discover -s tests -p "*_test.py" |
-t |
Top-level directory used for imports | python -m unittest discover -s tests -t . |
A common layout is:
project/
├── calculator.py
└── tests/
├── __init__.py
└── test_calculator.py
From project/, run:
python -m unittest discover -s tests -p "test*.py" -t .
There is no single mandatory project layout. The important requirements are that the discovered modules match the pattern and that Python can import both the test module and the code under test.
4. Fixtures: setup and cleanup around tests
A fixture represents the preparation needed to perform one or more tests. Fixtures are useful for temporary directories, proxy databases, server processes, and other resources that must be created and cleaned up reliably. Keep setup and cleanup explicit so one test does not depend on another.
Per-test setup and cleanup
import unittest
class UserTests(unittest.TestCase):
def setUp(self):
self.users = {"ada": {"active": True}}
def tearDown(self):
self.users.clear()
def test_active_user_is_present(self):
self.assertIn("ada", self.users)
self.assertTrue(self.users["ada"]["active"])
def test_missing_user_is_not_present(self):
self.assertNotIn("grace", self.users)
setUp runs before each test method and tearDown runs afterward. Put state that must be fresh for every test in these methods.
Class-level setup and cleanup
import unittest
class ServiceTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.service = start_test_service()
@classmethod
def tearDownClass(cls):
cls.service.close()
def test_health_endpoint(self):
self.assertEqual(self.service.health(), "ok")
Use class-level fixtures only for resources that can safely be shared by every test in the class. If setup raises an exception, cleanup must still be designed carefully; for resources that need guaranteed cleanup, register cleanup actions as soon as each resource is created.
Subtests for related inputs
import unittest
class AddTests(unittest.TestCase):
def test_examples(self):
examples = [(1, 2, 3), (0, 4, 4), (-2, 5, 3)]
for left, right, expected in examples:
with self.subTest(left=left, right=right):
self.assertEqual(left + right, expected)
A subtest keeps related cases in one method while reporting which input failed.
5. Assertions you will use most often
| Assertion | Checks |
|---|---|
assertEqual(actual, expected) |
Values are equal |
assertNotEqual(a, b) |
Values differ |
assertTrue(value) / assertFalse(value) |
Boolean truth |
assertIs(value, expected) |
Object identity |
assertIsNone(value) |
Value is None |
assertIn(item, container) |
Membership |
assertRaises(Error) |
An exception is raised |
assertAlmostEqual(a, b) |
Numerical values are close |
Add a msg= argument when a failure needs extra context:
self.assertEqual(status, 200, msg=f"unexpected status for {url}")
6. Import paths and package layout
Because discovery imports test modules, the directory from which you run the command matters. Run commands from the project’s intended top-level directory, and make the package structure explicit when needed.
If a test file is found but imports fail, check:
- the current working directory;
- the value of
-tsupplied to discovery; - whether package directories contain the expected
__init__.pyfiles for your layout; - whether the import is using the intended module name;
- whether an installed copy of your package is being imported instead of the source checkout.
When changes appear to have no effect, print the loaded module location:
import calculator
print(calculator.__file__)
This reveals which copy Python imported. An installed package elsewhere on the import path can hide the file you just edited.
7. A complete small project example
Application code:
# temperature.py
def celsius_to_fahrenheit(value):
return (value * 9 / 5) + 32
def fahrenheit_to_celsius(value):
return (value - 32) * 5 / 9
Tests:
# tests/test_temperature.py
import unittest
from temperature import celsius_to_fahrenheit, fahrenheit_to_celsius
class TemperatureTests(unittest.TestCase):
def test_freezing_point(self):
self.assertEqual(celsius_to_fahrenheit(0), 32)
def test_boiling_point(self):
self.assertEqual(celsius_to_fahrenheit(100), 212)
def test_round_trip(self):
for value in (-40, 0, 20, 100):
with self.subTest(value=value):
converted = fahrenheit_to_celsius(celsius_to_fahrenheit(value))
self.assertAlmostEqual(converted, value)
if __name__ == "__main__":
unittest.main()
Run it from the directory containing temperature.py:
python -m unittest discover -s tests -p "test*.py" -t . -v
8. Common failures and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Ran 0 tests |
Filename, class, or method does not match discovery conventions | Use a test*.py filename and methods beginning with test; pass -s and -p explicitly. |
ModuleNotFoundError |
Wrong working directory or import top level | Run from the project root and set -t . where appropriate. |
| Old code is imported | An installed package shadows the checkout | Inspect module.__file__, then correct the environment or install the project in the intended mode. |
| Tests pass alone but fail together | Shared mutable state or incomplete cleanup | Reset state in setUp, release resources in cleanup methods, and make each test independent. |
| Expected exception is not detected | The code is called outside the assertion context | Put the call inside with self.assertRaises(...):. |
| Floating-point equality fails | Binary floating-point rounding | Use assertAlmostEqual with an appropriate precision. |
| Discovery works locally but not in CI | Different Python version, working directory, or package installation | Record the Python version, use an explicit discovery command, and make the import root deterministic. |
9. Reliability and performance practices
- Keep tests independent so they can run in any order.
- Prefer deterministic inputs and avoid relying on wall-clock timing.
- Use fixtures to isolate temporary files, databases, and server processes.
- Keep unit tests focused on one behavior; move expensive integration work to separately named tests and commands.
- Run a focused method while editing, then run the complete discovery command before committing.
- Do not infer test quality from the number of tests alone; check whether important behavior and failure paths are covered.
The standard library does not require a separate test runner installation. Your project’s runtime, import layout, and external resources remain the main sources of setup and execution cost.

10. Can pytest run unittest tests?
Yes. pytest documents running tests written as unittest.TestCase classes and describes using pytest’s fixture mechanism when running them. Treat this as a compatibility option: a team can retain existing unittest cases while adopting pytest features where that helps its project. See the pytest unittest documentation and verify the current documentation for the pytest version you use.
The choice should follow project constraints, test organization preferences, and migration cost. The compatibility fact does not establish that one framework is universally faster or better.
11. Or skip the browser setup
If your tests also need website screenshots, ScreenshotNeo provides a GET endpoint that returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for the available options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers identify the page verdict and whether the request was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The Free plan includes 1,000 screenshots each month without a card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
12. Frequently asked questions
Do test methods have to start with test?
They do for the default discovery and test loader conventions. Keep the prefix unless you intentionally configure a different loading approach.
Should every test class have setUp and tearDown?
No. Add fixtures only when a test needs shared preparation or cleanup. Unneeded fixtures make tests harder to understand.
Why does discovery find a file but not its tests?
Discovery imports the module. A naming mismatch, import error, wrong top-level directory, or package shadowing can prevent the expected tests from loading.
Can one test depend on another test’s data?
It should not. Keep each case self-contained so it can run alone or in arbitrary combinations.
What command should CI use?
Use an explicit command such as python -m unittest discover -s tests -p "test*.py" -t ., adjusted to your package layout, so the search and import roots are visible in configuration.


