py-test abstract classes
In certain scenarios comes a need to create abstract classes. Those can be usefull as base classes, to provide a layout for different implementions of inheriting concrete versions. Class is called abstract when it contains at least one abstract method. Such a method simply lacks implementation, just provides a declaration. Due to that, abstract classes cannot be instantiated. So a question arrises – how to unit test their functionality, with python, without the ability to create instances?
Contents
HOW TO
One may ask, why to even test abstract classes, as their aim is to be inherited. First of all, abstract classes can contain some non-abstract methods, and those are the ones that we can test. Secondly, testing abstract classes does not assume not testing inheriting classes. One still should validate whether concrete classes are written correctly. Especially that while inheriting, we can also change implementation of a concrete method from the base abstract class.
One way to approach the unit tests topic in python is to simulate concrete implementation of an abstract base class, by creating a dummy class. Let’s consider 2 scenarions – testing of an independent and dependent concrete method. In both cases, abstract base class is defined using ABC library.
1. Testing of a independent method.
Consider following abstract class. It contains one abstract and one concrete method:
1 2 3 4 5 6 7 8 9 |
from abc import ABC, abstractmethod class MyAbstractClass(ABC): @abstractmethod def abstract_hello(self) -> str: pass def concrete_hello(self, name: str) -> str: return f"Hello {name}." |
To test the concrete one, let’s create a Dummy inheriting class, by providing a definition for abstract_hello() method. By not specifying concrete_hello(), we assume the default definition of this method:
1 2 3 |
class MyDummyClass(MyAbstractClass): def abstract_hello(self) -> str: return "" |
Now we can use this dummy class to test concrete_hello() method. I am using pytest library.
1 2 3 |
import pytest def test_hello(): assert MyDummyClass().concrete_hello("Alice") == "Hello Alice." |
This helps us determine that “default” provided method implementation works as expected. That is especially useful if such a logic is more complex. Or in a case where we would like to show how we intend this method to be used in inheriting classes. Of course it is a good practice to validate this method regardless whether subclass implements it differently or not. So let’s consider a concrete class this time:
1 2 3 |
class MyConcreteClass(MyAbstractClass): def abstract_hello(self) -> str: return "What a beautiful day!" |
And test both methods:
1 2 3 |
def test_hello(): assert MyConcreteClass().concrete_hello("Alice") == "Hello Alice." assert MyConcreteClass().abstract_hello() == "What a beautiful day!" |
2. Testing of a dependent method.
Now let’s change the implementation of the base class to make concrete method dependent on the abstract one:
1 2 3 4 5 6 7 |
class MyAbstractClass(ABC): @abstractmethod def abstract_hello(self) -> str: pass def concrete_hello(self, name: str) -> str: return f"Hello {name}. " + self.abstract_hello() |
In such a case testing of the concrete_hello() is very much dependent on the abstract method implementation. Let’s provide the dummy, not disturbing the logic:
1 2 3 |
class MyDummyClass(MyAbstractClass): def abstract_hello(self) -> str: return "" |
Our test can only follow the same logic:
1 2 |
def test_hello(): assert MyDummyClass().concrete_hello("Alice") == "Hello Alice." |
However this time it is more clear why we should retest this method in every subclass. That is because outcome of the concrete method very much depends on the abstract method and hence will be different for each implementation.
Testing abstract classes may seem as an overhead, as we should either way test thoroughly all derived subclasses. However if such a base class is to be used to constantly add new implementations, it is a good practice to validate all default implementations. By providing dummy instances, we can unit test methods in a relatively simple manner.