Python Testing — unittest & pytest
Automated tests check that your code does what you expect — and keep doing it as the code changes. Python ships unittestunittest in the standard library, and the third-party pytestpytest is the most popular framework in the ecosystem.
def add(a, b):
return a + b
# A test is just code that checks an assumption:
assert add(2, 3) == 5
assert add(-1, 1) == 0
print("all good")def add(a, b):
return a + b
# A test is just code that checks an assumption:
assert add(2, 3) == 5
assert add(-1, 1) == 0
print("all good")The idea: Arrange, Act, Assert
Most tests follow three steps:
- Arrange — set up inputs and state.
- Act — call the thing under test.
- Assert — check the result matches what you expect.
unittest (standard library)
unittestunittest groups tests into classes that subclass TestCaseTestCase. Each method named test_*test_* is a test, and you check results with assert*assert* methods.
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_positive(self):
self.assertEqual(add(2, 3), 5)
def test_negative(self):
self.assertEqual(add(-1, -1), -2)
def test_type(self):
self.assertIsInstance(add(1, 2), int)
if __name__ == "__main__":
unittest.main()import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_positive(self):
self.assertEqual(add(2, 3), 5)
def test_negative(self):
self.assertEqual(add(-1, -1), -2)
def test_type(self):
self.assertIsInstance(add(1, 2), int)
if __name__ == "__main__":
unittest.main()Run it from the terminal:
$ python -m unittest test_math.py
...
Ran 3 tests in 0.001s
OK$ python -m unittest test_math.py
...
Ran 3 tests in 0.001s
OKCommon assert methods
| Method | Passes when |
|---|---|
assertEqual(a, b)assertEqual(a, b) | a == ba == b |
assertNotEqual(a, b)assertNotEqual(a, b) | a != ba != b |
assertTrue(x)assertTrue(x) / assertFalse(x)assertFalse(x) | xx is truthy / falsy |
assertIsNone(x)assertIsNone(x) | x is Nonex is None |
assertIn(a, b)assertIn(a, b) | a in ba in b |
assertIsInstance(a, cls)assertIsInstance(a, cls) | aa is an instance of clscls |
assertRaises(Err)assertRaises(Err) | The block raises ErrErr |
setUp and tearDown
setUpsetUp runs before each test, tearDowntearDown after — great for shared fixtures.
import unittest
class TestList(unittest.TestCase):
def setUp(self):
self.data = [1, 2, 3] # fresh for every test
def test_append(self):
self.data.append(4)
self.assertEqual(self.data, [1, 2, 3, 4])
def test_length(self):
self.assertEqual(len(self.data), 3)import unittest
class TestList(unittest.TestCase):
def setUp(self):
self.data = [1, 2, 3] # fresh for every test
def test_append(self):
self.data.append(4)
self.assertEqual(self.data, [1, 2, 3, 4])
def test_length(self):
self.assertEqual(len(self.data), 3)Testing exceptions
import unittest
def divide(a, b):
return a / b
class TestDivide(unittest.TestCase):
def test_zero(self):
with self.assertRaises(ZeroDivisionError):
divide(1, 0)import unittest
def divide(a, b):
return a / b
class TestDivide(unittest.TestCase):
def test_zero(self):
with self.assertRaises(ZeroDivisionError):
divide(1, 0)pytest (third-party)
pytestpytest lets you write tests as plain functions using ordinary assertassert. Install it first:
$ pip install pytest$ pip install pytest# Functions named test_* are collected automatically.
def add(a, b):
return a + b
def test_positive():
assert add(2, 3) == 5
def test_negative():
assert add(-1, 1) == 0# Functions named test_* are collected automatically.
def add(a, b):
return a + b
def test_positive():
assert add(2, 3) == 5
def test_negative():
assert add(-1, 1) == 0$ pytest
==== 2 passed in 0.01s ====$ pytest
==== 2 passed in 0.01s ====Parametrize — one test, many cases
import pytest
def square(n):
return n * n
@pytest.mark.parametrize("value,expected", [
(2, 4),
(3, 9),
(-4, 16),
])
def test_square(value, expected):
assert square(value) == expectedimport pytest
def square(n):
return n * n
@pytest.mark.parametrize("value,expected", [
(2, 4),
(3, 9),
(-4, 16),
])
def test_square(value, expected):
assert square(value) == expectedFixtures — reusable setup
import pytest
@pytest.fixture
def sample_data():
return [1, 2, 3]
def test_sum(sample_data): # the fixture is injected by name
assert sum(sample_data) == 6import pytest
@pytest.fixture
def sample_data():
return [1, 2, 3]
def test_sum(sample_data): # the fixture is injected by name
assert sum(sample_data) == 6Testing exceptions in pytest
import pytest
def divide(a, b):
return a / b
def test_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)import pytest
def divide(a, b):
return a / b
def test_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)unittest vs pytest
| unittest | pytest | |
|---|---|---|
| Ships with Python | Yes | No (pip install pytestpip install pytest) |
| Test style | TestCaseTestCase classes | Plain functions |
| Assertions | self.assertEqual(...)self.assertEqual(...) | plain assertassert |
| Fixtures | setUpsetUp/tearDowntearDown | @pytest.fixture@pytest.fixture |
| Parametrize | manual / subTest | @pytest.mark.parametrize@pytest.mark.parametrize |
Both are excellent.
unittestunittestis always available;pytestpytestis more concise and has a rich plugin ecosystem. Many projects write plainassertasserttests and run them withpytestpytest.
Common pitfalls
- Test functions/methods must start with
testtestor the runner skips them. - Test one thing per test — small, focused tests pinpoint failures.
- Don’t depend on test order — each test should stand alone.
assertEqual(a, b)assertEqual(a, b)argument order — convention is(actual, expected)(actual, expected).
Practice Exercises
Exercise 1 – Assert a function’s result
Exercise 2 – assertEqual in a TestCase
Exercise 3 – Expect an exception
Summary
- Tests arrange, act, assert to lock in expected behaviour.
unittestunittest(built in) usesTestCaseTestCaseclasses,assert*assert*methods, andsetUpsetUp/tearDowntearDown.pytestpytest(install with pip) uses plain functions, bareassertassert, fixtures, and@parametrize@parametrize.- Test exceptions with
assertRaisesassertRaises/pytest.raisespytest.raises. - Keep tests small, independent, and named
test_*test_*.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
