Write Once, Test Everywhere: Sharing Common Tests in Python’s unittest
A while back, I was writing tests for a project using Python’s unittest. Nothing dramatic — just the usual rhythm: write a class, test it, move to the next one. After a few classes, though, the déjà vu hit.
Every test looked almost identical — the same setup, the same assertions, just different class names.
That’s when I paused and thought:
If all these classes share the same behavior, why am I writing the same tests over and over
So I started experimenting. What if I could write the tests once, and then somehow have them apply to multiple classes automatically? It sounded like something unittest should make easy — but it doesn’t. There’s no built-in way to define a test base class that only runs when subclassed. Still, there are elegant, Pythonic patterns that make it possible. In this article, I’ll walk you through how I turned that “boring moment” into a neat discovery — by creating shared test structures that reduce boilerplate, increase coverage, and make testing a bit more satisfying.
Setting the Stage#
- Let’s start with a simple, familiar scenario:testing some geometric shape classes that all share a common interface — each one implements an area() method.
- We’ll use it to explore several techniques for writing shared tests in Python’s unittest, from the classic base-class trick to more creative, scalable patterns.
Example:
import math
class Circle:
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius**2
class Square:
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side * self.side
class Triangle:
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def area(self) -> float:
return 0.5 * self.base * self.height
These are simple, but they share one clear contract:
Each class must implement .area() that returns a positive float.
Let’s see a few approaches to test this elegantly.
The base class and del trick#
import unittest
class BaseShapeTests(unittest.TestCase):
shape_class = None
init_args = ()
def test_area_returns_float(self):
shape = self.shape_class(*self.init_args)
self.assertIsInstance(shape.area(), float)
def test_area_is_positive(self):
shape = self.shape_class(*self.init_args)
self.assertGreater(shape.area(), 0)
class CircleTests(BaseShapeTests):
shape_class = Circle
init_args = (2,)
class SquareTests(BaseShapeTests):
shape_class = Square
init_args = (3,)
class TriangleTests(BaseShapeTests):
shape_class = Triangle
init_args = (4, 5)
del BaseShapeTests
del ensures the base class itself isn’t mistakenly collected by unittest.
A mixin approach#
class ShapeBehaviorMixin:
def test_area_positive(self):
shape = self.shape_class(*self.init_args)
self.assertGreater(shape.area(), 0)
def test_area_numeric(self):
shape = self.shape_class(*self.init_args)
self.assertIsInstance(shape.area(), (int, float))
class CircleTests(ShapeBehaviorMixin, unittest.TestCase):
shape_class = Circle
init_args = (2,)
class SquareTests(ShapeBehaviorMixin, unittest.TestCase):
shape_class = Square
init_args = (3,)
The tradeoff with this is you will have to to fight with type checkers like mypy about missing assert* methods in the mixin.
Hybrid pytest using test = False#
If you use pytest to run unittest suites, you can hide the base class gracefully:
class ShapeBaseTests(unittest.TestCase):
__test__ = False
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.__test__ = True
def test_area_contract(self):
shape = self.shape_class(*self.init_args)
self.assertGreater(shape.area(), 0)
Conclusion#
In conclusion ,that moment when you catch yourself repeating a test is the perfect time to step back and design for reuse. Sharing tests isn’t just about saving keystrokes — it’s about expressing intent. It makes your test suite more declarative: Every class that behaves like this must pass these checks.
When your code shares behavior, your tests should too. Write them once, share them widely, and let unittest do the heavy lifting.
Don’t repeat yourself — not even in your tests.