As a 30-year software developer, mostly in OO languages, but a newbie at python, I’m looking to find what is best practise for isolating unit tests in python.
Let’s say I have the following, semi-pseudo-code. I know some_database_client isn’t a real database client:
from some_database_client import connection
connection.connect('server_id', 'username', 'password')
def function_under_test():
return connection.get_value('some_value_reference') + 10
If I write a test for function_under_test()
, then I’m effectively testing the potentially enormous amount of logic in connecting and retrieving the data from connection
.
Instead, I really want to test that function_under_test()
adds ten to the value retrieved.
This relates to a practical real-world example where the module-level variable connection
(or it’s real-world equivalent) is referenced heavily all over the code.
Therefore, it would involve a large and therefore very risky change to pass connection
as an argument to all the functions that use it. Without doing this though, I can’t easily separate the variable connection
when I’m trying to unit-test the functions that use it. connection
is instantiated before I even get to run the unit-test.
Is there a recommended way to isolate functions like this for testing? I can certainly think of many, many ways, but I suspect that some have more of the “Zen of Python” about them than others. e.g. Wrapping everything in classes is a possibility, but that is perhaps moving too far away from keeping stuff simple, which is a goal of python.
8
This relates to a practical real-world example where the module-level variable connection (or it’s real-world equivalent) is referenced heavily all over the code. Therefore, while it would involve a large and therefore very risky change to pass connection as an argument to all the functions that use it.
That’s the only clean solution. Too late now, but in the future avoid such hidden dependencies if possible. The only other option I can think of is to change the Python path to point to an alternate some_database_client
with a stub connection
when you run your unit tests.
However…
Instead, I really want to test that function_under_test() adds ten to the value retrieved.
You don’t really need to test this. You can prove the correctness of such a trivial function by inspection. Writing unit tests to show it adds ten regardless of whether the return value of connection is 0, positive, negative, odd or even is slower than just looking at it and it still doesn’t guarantee correctness. Knowing what not to test is just as important as knowing what to test.
2
First, initializing a global connection at the module level is really, really bad practice. Rather, if you have to do that, you should have something like
def connect():
connection.connect('server_id', 'username', 'password')
if __name__ == '__main__':
connect()
So that you only actually connect in main execution.
Fortunately, from your code example, even if you connect to the database at the start of the module, you can just ignore that assignment and assign a new mock connection in your test.
def my_test():
connection = MockConnection()
function_under_test()
assert(...)
FWIW I found Python really easy to test because if there’s some artifact, dependency, or function that was hard to work with, I’d just shove in an implementation that let me focus on the test.
3
It intrinsically has to do with a basic yet important OOP concept: Inversion of control.
One of the ways to reach it is Dependency Injection. In a few words, instead of calling a determined function specified in the body of the function, you’ll call a function given by the caller.
Introducing it to your problem:
def function_under_test(connection):
return connection.get_value('some_value_reference') + 10
Now, we need to pass an object that has the method get_value. In production, you’d pass the real connection object. When testing, however, you’d pass a mock (fake version) of it.
An example of how easy testing becomes:
class FakeConnection():
def get_value(self, reference):
return 30
def test_function():
connection = FakeConnection()
result = function_under_test(connection)
assert result == 40