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()
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..."""
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..."""
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()
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)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.