How are unit tests decorated with features?

I have been trying lately to learn a lot in the best methods for testing modules. Most of them make sense, but there is something that is often overlooked and / or poorly explained: how should one test element decorate functions?

Suppose I have this code:

def stringify(func): @wraps(func) def wrapper(*args): return str(func(*args)) return wrapper class A(object): @stringify def add_numbers(self, a, b): """ Returns the sum of `a` and `b` as a string. """ return a + b 

I obviously can write the following tests:

 def test_stringify(): @stringify def func(x): return x assert func(42) == "42" def test_A_add_numbers(): instance = MagicMock(spec=A) result = A.add_numbers.__wrapped__(instance, 3, 7) assert result == 10 

This gives me 100% coverage: I know that any function that is decorated with stringify() gets its result as a string, and I know that the undecorated A.add_numbers() function returns the sum of its arguments. Thus, in terms of transitivity, the issued version of A.add_numbers() should return the sum of its argument as a string. Everything seems good!

However, I'm not quite happy with this: my tests, as I wrote, can still pass if I use a different decorator (which does something else, multiply the result by 2 instead of casting to str ). My A.add_numbers function A.add_numbers not be correct, but the tests will pass anyway. Not surprising.

I could test the decorated version of A.add_numbers() , but then I would try everything, since my decorator was already tested for one.

It seems that I missed something. What is a good strategy for unit tests of decorated functions?

+5
source share
3 answers

In the end, I divided my decorators into two parts. Therefore, instead of:

 def stringify(func): @wraps(func) def wrapper(*args): return str(func(*args)) return wrapper 

I have:

 def to_string(value): return str(value) def stringify(func): @wraps(func) def wrapper(*args): return to_string(func(*args)) return wrapper 

This allows me later to simply shout out to_string while testing the decorated function.

Obviously, in this simple example this may seem redundant, but when used with a decorator that actually does something complicated or expensive (for example, opening a database connection or something else), the ability to mock it is a very nice thing .

+1
source

One of the main advantages of unit testing is the ability to refactor with some degree of confidence that the reorganized code continues to work as before. Suppose you started with

 def add_numbers(a, b): return str(a + b) def mult_numbers(a, b): return str(a * b) 

You would have tests like

 def test_add_numbers(): assert add_numbers(3, 5) == "8" def test_mult_numbers(): assert mult_numbers(3, 5) == "15" 

Now you decide to reorganize the common parts of each function (wrapping the output in a string) using the stringify decoder.

 def stringify(func): @wraps(func) def wrapper(*args): return str(func(*args)) return wrapper @stringify def add_numbers(a, b): return a + b @stringify def mult_numbers(a, b): return a * b 

You will notice that your original tests continue to work after this refactoring. It doesn't matter how you implemented add_numbers and mult_numbers ; what matters is that they continue to work as defined: while maintaining a rigorous result of the desired operation.

The only remaining test you need to write is to verify that stringify does what it is intended to: return the result of the decorated function as a string that your test_stringify .


Your problem is that you want to consider the expanded function, decorator and wrapped function as units. But if that is the case, then you are missing one unit test: one that actually runs add_wrapper and checks its output, not just add_wrapper.__wrapped__ . It doesn’t really matter if you consider testing a completed function as a unit test or an integration test, but whatever you call it, you need to write it because, as you pointed out, it is not enough to check only the expanded function and decorator separately.

+1
source

Check the open interface of your code. If you only expect people to call decorated functions, then this is something you should check out. If the decorator is also public, check it out too (as you did with test_stringify() ). Do not check wrapped versions unless people name them directly.

0
source

All Articles