pytest classes are a way to group related tests together, offering a more organized and less boilerplate-heavy alternative to Python’s built-in unittest module.
Imagine you have a set of tests for a specific feature, say, user authentication. You’d want these tests to live together. In unittest, this means creating a class that inherits from unittest.TestCase. Pytest allows this too, but it also lets you group tests without the inheritance requirement, which is where the "bloat" comes from.
Let’s see this in action. Suppose we have a simple calculator module:
# calculator.py
class Calculator:
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
def multiply(self, a, b):
return a * b
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
Now, let’s write tests for this.
Without Explicit Classes (Pytest’s Default):
# test_calculator_simple.py
from calculator import Calculator
def test_add():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_subtract():
calc = Calculator()
assert calc.subtract(5, 2) == 3
def test_multiply():
calc = Calculator()
assert calc.multiply(4, 3) == 12
def test_divide():
calc = Calculator()
assert calc.divide(10, 2) == 5
def test_divide_by_zero():
calc = Calculator()
with pytest.raises(ValueError):
calc.divide(5, 0)
This works perfectly fine. Pytest discovers all functions starting with test_.
Using Pytest Classes for Organization:
Now, let’s say we want to group these calculator tests. We can create a class for them.
# test_calculator_class.py
import pytest
from calculator import Calculator
class TestCalculator:
def setup_method(self, method):
# This runs before each test method in this class
self.calc = Calculator()
def teardown_method(self, method):
# This runs after each test method in this class
pass # Nothing to clean up here
def test_add(self):
assert self.calc.add(2, 3) == 5
def test_subtract(self):
assert self.calc.subtract(5, 2) == 3
def test_multiply(self):
assert self.calc.multiply(4, 3) == 12
def test_divide(self):
assert self.calc.divide(10, 2) == 5
def test_divide_by_zero(self):
with pytest.raises(ValueError):
self.calc.divide(5, 0)
When you run pytest, it will discover TestCalculator and then run each method within it that starts with test_. The setup_method and teardown_method are hooks that pytest provides. setup_method is called before each test method, and teardown_method is called after. This is where you’d typically instantiate objects or set up common resources for your tests.
The "bloat" you avoid with pytest classes compared to unittest is the need to inherit from unittest.TestCase and the more verbose assertion syntax (e.g., self.assertEqual(a, b) vs. assert a == b). Pytest also automatically discovers classes that start with Test (or test_ if you configure it) and their methods starting with test_.
The Mental Model:
- Grouping: Classes help you logically group tests for a specific module, feature, or component. This makes your test suite easier to navigate and understand.
- Setup/Teardown: Pytest’s
setup_methodandteardown_method(orsetup_classandteardown_classfor class-level setup/teardown) are powerful for managing test state.setup_method: Runs before each test method. Good for creating fresh instances of objects or resetting state for every single test.teardown_method: Runs after each test method. Good for cleaning up resources created insetup_method.setup_class: Runs once before any test method in the class. Good for expensive setup that can be shared across all tests in the class.teardown_class: Runs once after all test methods in the class. Good for cleaning up resources created insetup_class.
- Fixtures: While not strictly part of "pytest classes," fixtures are the more idiomatic pytest way to handle setup and teardown, often replacing
setup_methodandteardown_method. They offer more flexibility and reusability. You can use fixtures within your test classes as well.
The Counterintuitive Bit:
Pytest test classes don’t have to inherit from anything, which is a major departure from unittest. You can just define a class whose name starts with Test (or test_) and pytest will find it. This means you can create a class purely for organizational purposes, and if you don’t need class-level setup or teardown, you don’t need to define any special methods. The tests are just methods within that class. This flexibility is key to avoiding the perceived "bloat" of traditional testing frameworks.
If you do want to use unittest.TestCase for compatibility or preference, pytest supports that too. Just inherit from unittest.TestCase as usual, and pytest will discover and run those tests as well, still benefiting from pytest’s richer assertion introspection and plugin ecosystem.
The next step is often exploring how fixtures can further enhance your test class organization by providing reusable setup and data for your tests.