Sometimes we have some interface and we would like to write a generic test suite for it, so that concrete implementations just reuse it.
The problem with Python's unittest
(and transitively absltest
) is that the TestCase
class serves as a "marker" telling the test executor that it should be executed. This is unfortunate, because abstract test suites should not run. Consider this example:
class FooTest(absltest.TestCase):
@abstractmethod
def new_foo(self) -> Foo:
pass
def test_bar(self) -> None:
foo = self.new_foo()
self.assertEqual(foo.bar(), "bar")
class QuuxTest(FooTest):
def new_foo(self) -> Foo:
return Quux()
class NorfTest(FooTest):
def new_foo(self) -> Foo:
return Norf()
Here, the test executor will instantiate FooTest
, QuuxTest
and NorfTest
. However, FooTest
is abstract and it is not possible to create an instance of it. There are three workarounds for this that I am aware of.
The first one is to configure the test executor to ignore specific test classes or prefixes (possible in pytest). However, this is awkward and requires modifying external configuration files.
The second one is described here. Basically, we del
the base class once child classes are defined. This feels very wrong and works only if all the concrete test cases are defined in the same module.
The last one is to use a mixin approach. Instead of making FooTest
derive from absltest.TestCase
, we "mark" only concrete classes with it:
class FooTest:
@abstractmethod
def new_foo(self) -> Foo:
pass
def test_bar(self) -> None:
foo = self.new_foo()
self.assertEqual(foo.bar(), "bar")
class QuuxTest(FooTest, absltest.TestCase):
def new_foo(self) -> Foo:
return Quux()
class NorfTest(FooTest, absltest.TestCase):
def new_foo(self) -> Foo:
return Norf()
The problem here is that it doesn't work with static type checkers: FooTest
now doesn't inherit from TestCase
but uses methods like self.assertEqual
.
This last solution seems like the only "correct" one except for the mentioned issue. Instead, Abseil could define an abstract test class and make the normal test case class implement it:
class AbstractTestCase(ABC):
@abstractmethod
def assertEqual(self, this, that):
...
...
class TestCase(AbstractTestCase):
...
Then, it would be possible to inherit from absltest.AbstractTestCase
in the abstract test case, making the type checker happy:
class FooTest(absltest.AbstractTestCase):
@abstractmethod
def new_foo(self) -> Foo:
pass
def test_bar(self) -> None:
foo = self.new_foo()
self.assertEqual(foo.bar(), "bar")
class QuuxTest(FooTest, absltest.TestCase):
def new_foo(self) -> Foo:
return Quux()
class NorfTest(FooTest, absltest.TestCase):
def new_foo(self) -> Foo:
return Norf()