Ronan's Tech Blog

Feb 24, 2019

Testing interface contracts in Python

I've been writing some interface heavy code in Python recently and have developed a pattern that I really like and have not seen before. I think this is original but I doubt it's the first. I'd like to hear about any similar approaches that have been documented.

Interfaces in Python

They aren't used as widely as they could be, owing to the dynamic nature of Python and it's tendency towards duck typing.

Still, there's some very good support for them in community tooling. I make very good use of pylint for detecting interface completion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# src/interface/knight.py

class Knight(object):
    def say_ni(self):
        raise NotImplementedError

    def set_able(self, is_able):
        raise NotImplementedError

    def drink(self):
        raise NotImplementedError


class NotAbleToDrinkError(Exception):
    pass

pylint will detect that this is an abstract method and if your subclasses fail to implement it then it says so. This is nice but isn't nearly enough to guarantee correct behaviour.

Interface contracts

The contract is our specification for how an interface must behave. In the case of our Knight, the knights must be able to say "ni" and they must drink whenever they are able.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# src/implementation1/test_knight.py
from unittest import TestCase

from implementation_module import Knight


class TestKnightContract(TestCase):
    def test_can_say_ni_without_issues(self):
        knight = Knight()
        knight.say_ni()

    def test_drinks_when_able(self):
        knight = Knight()
        knight.set_able(True)
        knight.drink()

    def test_does_not_drink_when_unable(self):
        knight = Knight()
        knight.set_able(False)
        with self.assertRaises(NotAbleToDrinkError):
            knight.drink()

That's a simple test for a knight's interface contract. All good so far but it falls down when we have multiple implementations of our interface. We end up writing the same contract out again and again.

DRY - one contract better than repeated tests

Normally if someone recommends deduplicating test code "because it's more DRY" I feel compelled to remind them that DRY is about ideas, not about lines of code. In this case we are dealing with the same idea so we should re-use the same code. There is only one contract that all must obey.

My emerging pattern for implementation of this looks a little unusual - it's defining a class inside a method in order to bind the knight_factory into the closure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# src/interfaces/knight_contract.py

from unittest import TestCase

from .knight import NotAbleToDrinkError


def contract_test(knight_factory):
    class TestKnightContract(TestCase):
        def test_can_say_ni_without_issues(self):
            knight = knight_factory()
            knight.say_ni()

        def test_drinks_when_able(self):
            knight = knight_factory()
            knight.set_able(True)
            knight.drink()

        def test_does_not_drink_when_unable(self):
            knight = knight_factory()
            knight.set_able(False)
            with self.assertRaises(NotAbleToDrinkError):
                knight.drink()

    return TestKnightContract


# src/implementation1/test_knight.py

from ..interface.knight_contract import contract_test
from .knight import Knight


class TestKnight(contract_test(Knight)):
    pass

This pattern can now be used to implement the same contract in multiple places. When the contract needs clarifying with another test you can be sure that all implementations follow it.

Note that we subclass to create this test instead of simply TestKnight = contract_test(Knight). That simple assignment would run the tests but it would not report which implementation was failing when something broke.

A slight modification for Django

When writing these contracts for interfaces that can be backed by Django models I found a need to use Django's own TestCase implementation in order to have the test database set up correctly.

It's a small tweak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# src/interface/knight_contract.py

def contract_test(knight_generator, test_class=TestCase):
    class KnightContractTest(test_class):
        # On with the tests.


# django/tests/test_knight.py

class DjangoKnightTest(contract_test(Knight, test_class=django.test.TestCase)):
    pass

Problems?

As I said as the top, I haven't seen this done before. If there's an issue with this approach that I can't see then I'd love to hear about it.