0

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.

1

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.

  • Are you saying that the clean solution is to pass in (or inject) the connection? I would have thought so too, but I'm trying not to bring my pre-conceptions developed in other languages into python if it's inappropriate. I've only been developing python for two weeks. The "+ 10" is only a hypothetical example. I can guarantee I'm not testing anything quite so trivial. – user1738833 Mar 21 '14 at 17:05
  • @user1738833 Yeah, that's what I meant to say. It's almost always a good idea to expose your function's dependencies as arguments if they have some sort of state. Having some secret invisible state affecting the result of a function makes it harder to tell what a program is doing. – Doval Mar 21 '14 at 17:10
1

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.

  • I thought module level variables were immutable, so I couldn't modify "connection" within the context of the unit test? This is all inherited code. I'm only two weeks into programming python, but I can already see that the test framework in the code I inherited is woeful / non-existent. I'm trying to put one in place without any massive code changes - at least until I get the "Zen of Python" truly embedded in my head. – user1738833 Mar 21 '14 at 17:17
  • lol to quote Guido: "We're all adults here!" The only language I've used that has more flexible binding is Javascript. In Python, you can even add new functions to a module at runtime. The Python mock libraries are really just wrappers around standard tasks like assigning a new function definition to a module. Here's a trick that blew me away; since Python is compile-at-runtime, the source script for everything is somewhere on your system in the Python path, so you can just drill in and see exactly what it's doing. – Rob Mar 21 '14 at 17:38
  • In java, I feel good if I can push my unit tests above 90%. But in Python, it's really easy to hit 100%, because it's really simple to just drop in mocks for everything hard -- global variables, functions, modules, etc. – Rob Mar 21 '14 at 17:41
0

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.