How To Unit Test Abstract Base Classes In Python?
Python’s abstract base classes have at least one abstract method, so they cannot be instantiated directly. They are useful for defining laying out the class interface, but deferring the implementation to the inherting classes. Though a problem arises when you want to test the non-abstract methods of an abstract class. How do you do that if you cannot isntantiate the class?
There are two options to test an abstract base class (ABC) in Python: you can either override or patch the __abstractmethods__ property to be able to instantiate the abstract class and test it directly or you can create a child class that implements all the abstract methods of the base class.
In this tutorial I’ll explain exactly how you can implement the above.
1. Overriding the __abstractmethods__ property
Let’s say we have simple abstract class, called Example
:
# example.py
from abc import ABC, abstractmethod
class Example(ABC):
@abstractmethod
def abstract_method(self):
raise NotImplementedError
def concrete_method(self):
return True
If it were a regular class, and we wanted to test the implementation of concrete_method
, we’d do something like this:
# test.py
from unittest import TestCase
from unittest.mock import patch
from example import Example
class TestExample(TestCase):
def test_returns_1(self):
example = Example()
self.assertTrue(example.concrete_method())
However, as Example
is an abstract class, this test won’t pass, because Python will complain about not being able to instantiate it:
$ python -m unittest test.py
E
======================================================================
ERROR: test_concrete_method (test.TestExample)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 7, in test_returns_1
example = Example()
TypeError: Can't instantiate abstract class Example with abstract methods abstract_method
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
But how does the interpreter know that our class is abstract. Turns out, that behind the scenes, it checks the __abstractmethods__
property. The Example.__abstractmethods__
field contains a set of the names of all the abstract methods defined on Example
. If this set is empty however, the Python interpreter will happily instantiate our class without any problem.
It means that we can easily trick the Python interpreter by overwriting this property of the class that we want to test:
# test.py
from unittest import TestCase
from unittest.mock import patch
from example import Example
class TestExample(TestCase):
@patch("example.Example.__abstractmethods__", set())
def test_returns_1(self):
example = Example()
self.assertTrue(example.concrete_method())
Warning: don’t use this hack in production, it defeats the purpose of marking the methods abstract.
2. Adding A Dummy Implemention Of The Abstract Methods
Another way to go about testing a Python abstract class is to create a simple concrete child class just for testing purposes. For demonstration, let’s use the same Example
abstract class as before:
# example.py
from abc import ABC, abstractmethod
class Example(ABC):
@abstractmethod
def abstract_method(self):
raise NotImplementedError
def concrete_method(self):
return True
In the test file you can add a helper class that inherits from the abstract class that you want to test:
# test.py
from example import Example
class ConcreteExample(Example):
def abstract_method(self):
raise NotImplementedError
And then test the functionality of the base class via the child class:
# test.py - continued
from unittest import TestCase
from unittest.mock import patch
class TestExample(TestCase):
def test_returns_1(self):
example = ConcreteExample()
self.assertTrue(example.concrete_method())
Closing Thoughts
These were two basic ideas to help you test abstract classes in Python. Overwrite the magic property and bypass the interpreters safety measures to be able to instantiate an otherwise uninstantiable class or just provide a dummy implementation for the missing methods.
Sometimes real life can get a bit more complicated so you might need to combine the two approaches.
It is also possible that concrete methods rely on the abstract methods, so you will be forced to provide them some kind of implementation.