DEV Community

Cover image for Contract Breach
Montegasppα Cacilhας
Montegasppα Cacilhας

Posted on

Contract Breach

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

Among the most common 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 as hooks too – ’cause they are, remember? Python as a Framework…

In the Python’s unit test library, 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) 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:

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()
Enter fullscreen mode Exit fullscreen mode

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:

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..."""
Enter fullscreen mode Exit fullscreen mode

Suddenly the database connection stops working in the test!

The solution suggested by the first guy is using super:

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..."""
Enter fullscreen mode Exit fullscreen mode

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, just changing yield by super (here’s the super’s place):

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()
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)