Contract Breach
Tags: concept, python, testing, api
Published: 2020-08-23 02:48:55

[contextlib]: https://docs.python.org/3/library/contextlib.html
[datamodel]: https://docs.python.org/3/reference/datamodel.html
[dunning-kruger]: https://pubmed.ncbi.nlm.nih.gov/10626367/
[python]: https://python.org/
[sqlite]: https://www.sqlite.org/
[testcase]: https://docs.python.org/3/library/unittest.html#unittest.TestCase
[unittest]: https://docs.python.org/3/library/unittest.html

I’ve seen developers breaking established contracts, sometimes ’cause they don’t know the technology very well, sometimes due to [Dunning-Kruger effect][dunning-kruger].

Among the most common [Python][python] mistakes I’ve ever seen, there’s the [TestCase] contract breach.

### Hook method

The breach usually happens ’cause the programmer couldn’t understand the hook method concept, overriding the hook twice (or more), making necessary to call `super`.

A hook is a method called by some routine inside a framework. By definition, a hook shouldn’t be called by the application code. Instead, the framework calls it in a suitable flow.

Usual examples are the `update` and `render`/`draw` methods in a game framework. You can think about the magic methods from Python’s [data model][datamodel] as hooks too – ’cause they are, remember? Python as a Framework…

In the Python’s [unit test library][unittest], the `TestCase` class methods `setUp` and `tearDown` (instance methods), and `setUpClass` and `tearDownClass` (class methods) are hooks, called by the unit test framework.

### The problem

A junior programmer, very excited with his fresh knowledge about inheritance, decided to use it as many as possible, and he has just found a place where inheritance seems to fit like hand and glove: he needs to initialise a new clean memory database (using [SQLite][sqlite]) for each test, and clean it up before the next one.

So, the `setUp` hook is called before every test, and the `tearDown` hook is called after each one too, on success or failure. Perfect!

But he needs it to be in every test case class, so the solution he found is: creating his own `TestCase` class, subclassed by every test case class.

The thing goes like this:

```python
import unittest
import sqlite3
import my_app

__all__ = ['TestCase']


class TestCase(unittest.TestCase):

    def setUp(self):
        conn = my_app.conn = sqlite3.connect(':memory:')
        conn.execute("""CREATE TABLE t_user (
                            id INTEGER PRIMARY KEY AUTOINCREMENT,
                            name TEXT,
                            birth DATE,
                            register INTEGER
                        )""")
        conn.commit()

    def tearDown(self):
        my_app.conn.close()
```

Then he just needs to import this class instead of the original one as superclass of every tests, and problem solved. Until it’s not…

### The new issue

But in the real world one usually doesn’t work by oneself.

Other people in his team have to write their test cases too, and someone eventually doesn’t realise he can’t just use the hooks like established by the well-known original contract.

The new guy needs to patch Redis, and that’s what he does:

```python
from unittest.mock import patch
from my_test_case import TestCase
from my_app import CacheManager

__all__ = ['TestCacheManager']


class TestCacheManager(TestCase):

    def setUp(self):
        redis_patch = self.redis_patch = patch('my_app.Redis')
        self.redis = redis_patch.start()

    def tearDown(self):
        self.redis_patch.stop()

    """The tests go here..."""
```

Suddenly the database connection stops working in the test!

The solution suggested by the first guy is using `super`:

```python
from unittest.mock import patch
from my_test_case import TestCase
from my_app import CacheManager

__all__ = ['TestCacheManager']


class TestCacheManager(TestCase):

    def setUp(self):
        super().setUp()
        redis_patch = self.redis_patch = patch('my_app.Redis')
        self.redis = redis_patch.start()

    def tearDown(self):
        self.redis_patch.stop()
        super().tearDown()

    """The tests go here..."""
```

And that’s the **contract breach**. We should never have to call `super` inside a hook, ’cause parent hooks are essentially empty – or worst: they may raise an “not implemented” exception.

Thus this approach doesn’t fit the contract.

### Fixing it

Since the new `TestCase` class intends to change the framework behaviour, **override the framework internals**.

In this case, the method to be overridden is `run`. The code change is suchlike using `contextmanager` from [`contextlib`][contextlib], just changing `yield` by `super` (here’s the `super`’s place):

```python
class TestCase(unittest.TestCase):

    def run(self, result=None):
        conn = my_app.conn = sqlite3.connect(':memory:')
        conn.execute("""CREATE TABLE t_user (
                            id INTEGER PRIMARY KEY AUTOINCREMENT,
                            name TEXT,
                            birth DATE,
                            register INTEGER
                        )""")
        conn.commit()

        try:
            return super().run(result=result)
        finally:
            conn.close()
```

Done! Problem solved – for good.

Now there’s no more problem in overriding `setUp` or `tearDown` hooks, the database is still set, without calling `super` inside hooks.

### Aftermath

Those problems are fruit of misunderstanding the language and the contracts, easily solved by a lil’ research. Read the code! The Python libraries are very well commented.