Skip to content

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.

why_test.py
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")
why_test.py
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:

  1. Arrange — set up inputs and state.
  2. Act — call the thing under test.
  3. 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.

unittest_basic.py
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()
unittest_basic.py
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:

terminal
$ python -m unittest test_math.py
...
Ran 3 tests in 0.001s
OK
terminal
$ python -m unittest test_math.py
...
Ran 3 tests in 0.001s
OK

Common assert methods

MethodPasses 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.

setup_teardown.py
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)
setup_teardown.py
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

unittest_raises.py
import unittest
 
def divide(a, b):
    return a / b
 
class TestDivide(unittest.TestCase):
    def test_zero(self):
        with self.assertRaises(ZeroDivisionError):
            divide(1, 0)
unittest_raises.py
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:

terminal
$ pip install pytest
terminal
$ pip install pytest
test_pytest.py
# 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
test_pytest.py
# 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
terminal
$ pytest
==== 2 passed in 0.01s ====
terminal
$ pytest
==== 2 passed in 0.01s ====

Parametrize — one test, many cases

parametrize.py
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) == expected
parametrize.py
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) == expected

Fixtures — reusable setup

fixtures.py
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) == 6
fixtures.py
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) == 6

Testing exceptions in pytest

pytest_raises.py
import pytest
 
def divide(a, b):
    return a / b
 
def test_zero():
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)
pytest_raises.py
import pytest
 
def divide(a, b):
    return a / b
 
def test_zero():
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)

unittest vs pytest

unittestpytest
Ships with PythonYesNo (pip install pytestpip install pytest)
Test styleTestCaseTestCase classesPlain functions
Assertionsself.assertEqual(...)self.assertEqual(...)plain assertassert
FixturessetUpsetUp/tearDowntearDown@pytest.fixture@pytest.fixture
Parametrizemanual / subTest@pytest.mark.parametrize@pytest.mark.parametrize

Both are excellent. unittestunittest is always available; pytestpytest is more concise and has a rich plugin ecosystem. Many projects write plain assertassert tests and run them with pytestpytest.

Common pitfalls

  • Test functions/methods must start with testtest or 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) uses TestCaseTestCase classes, assert*assert* methods, and setUpsetUp/tearDowntearDown.
  • pytestpytest (install with pip) uses plain functions, bare assertassert, 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 coffee

Was this page helpful?

Let us know how we did