(This article didn't make it to the python advent calendar. It was full.)
It is a test that is executed by specifying parameters. I want to do something like the following.
def add(x, y):
return x + y
class Tests(unittest.TestCase):
def _callFUT(self, x, y):
return add(x, y)
@paramaterized([
(1, 2, 3),
(1, 1, 2),
(1, 0, 1),
(2, 3, 4),
(4, 4, 8)
])
def _test_add(self, x, y, expected):
"""Simple addition comparison"""
result = self._callFUT(x, y)
self.assertEqual(result, expected)
The execution result will look like the following. _test_add is called 5 times with different parameters.
...F.
======================================================================
FAIL: test_add_G4 (__main__.Tests)
Simple addition comparison: args=(2, 3), expected=4
----------------------------------------------------------------------
Traceback (most recent call last):
File "test-paramaterize.py", line 24, in test
return method(self, *candidates[n])
File "test-paramaterize.py", line 52, in _test_add
self.assertEqual(result, expected)
AssertionError: 5 != 4
----------------------------------------------------------------------
Ran 5 tests in 0.001s
FAILED (failures=1)
It's about how to create such a parameterized function.
A function decorator is a function that wraps a function. You can change the behavior of the function by wrapping it. It's basically a function that returns a function.
When the following Hello class was defined.
class Hello(object):
def __init__(self, name):
self.name = name
def hello(self):
return "hello {name}".format(name=self.name)
Hello("foo").hello() # hello foo
Define a double decorator like this:
def double(method):
def _double(self): #Since it is a method, take self
result = method(self)
return result + result
return _double
double is a decorator that outputs the result of the internal method twice. If you attach this to the hello method of the Hello class earlier, the behavior of Hello.hello () will change.
class Hello(object):
def __init__(self, name):
self.name = name
@double
def hello(self):
return "hello {name}".format(name=self.name)
print(Hello("foo").hello()) # hello foo.hello foo.
unitttest.TestCase recognizes methods starting with "test_" as test methods. Therefore, in order to convert to parameterized test, it is necessary to generate multiple test methods.
Let's try creating a class decorator and increase the number of test methods. The class decorator takes the class and returns the class. It's a class version of the function decorator.
import unittest
def add_foo_test(names):
def _add_foo_test(cls):
for name in names:
def test(self):
#Make a test that succeeds in text
self.assertEqual(1, 1)
test.__name__ = "test_{name}".format(name=name)
setattr(cls, test.__name__, test)
return cls
return _add_foo_test
@add_foo_test(["foo", "bar", "boo"])
class Tests(unittest.TestCase):
pass
unittest.main()
add_foo_test is a function that creates a test method that succeeds in texting using a list of passed names. This time, "foo", "bar", "boo" were passed, so test methods test_foo, test_bar, test_boo were created.
The result is as follows.
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
You are making three tests. Should I make something similar to this with a parameterized test? It means that.
frame object
But there are still problems. The parameterized function I wanted to create was a function that decorates the method. However, what I was making in the previous place was a decorator that decorates the class. It will not work as it is.
It looks like you need to set the value of the class from the method. This is something like retrieving a running call stack.
The call stack is the stack on which the environment is stacked during the execution of a function. It holds the environment at the time of each call. (Because of this, recursive calling works well.)
Let's take a look at an example. There are functions f and g. It's like g calls f. f is a function that returns a local variable at that time as a dictionary.
def f(x):
return locals() #Extract local variables in this environment as a dictionary.
def g(x):
r = f(x*x)
print(locals())
return r
print(g(10))
# {'r': {'x': 100}, 'x': 10} # g
# {'x': 100} # f
When I run it, only x is returned.
Let's add a local variable of g in f. At the time of calling, it has the following form, so you can see g from f by accessing the call stack.
[f] <-- current
[g]
[toplevel]
The way to touch it is to use a suspicious function called sys._getframe. As an argument, we will pass the number of frames you want from the current position.
[f] <- sys._getframe()Get in
[g] <- sys._getframe(1)Get in
[toplevel]
Let's add a variable called dummy. Change the code as follows.
import sys
def f(x):
sys._getframe(1).f_locals["dummy"] = "*dummy*" #Add dummy to the local variable of the next higher frame
return locals() #Extract local variables in this environment as a dictionary.
def g(x):
r = f(x*x)
print(locals())
return r
print(g(10)) # {'x': 100}
# {'r': {'x': 100}, 'x': 10, 'dummy': '*dummy*'} # g
# {'x': 100} # f
In this way, I was able to change the local variable in g in f.
What did it mean to touch the previous call stack? That's because the local variable in the frame one frame above the method decorator is the position of the class. In other words, by looking into the call stack in the parametrized function you were trying to create, you can access the class that has that method from within the function as the method decorator. Let me give you an example.
import sys
def add_A(method):
sys._getframe(1).f_locals["A"] = "AAAAAA"
return method
class Boo(object):
@add_A
def hello(self):
pass
print(Boo.A) # AAAAAA
The add_A decorator is a method decorator. You can access the class through the call stack. A class variable called A is set.
Let's summarize the knowledge so far.
--You can convert a method by attaching a decorator to the method. --The test method of the unittest module is a method that starts with "test_" --You can access the next higher call hierarchy via the call stack. --The position one level above the position when calling the method decorator is the class
You can now create a parameterized function by combining these. The following is the implementation.
import sys
i = 0
def gensym():
global i
i += 1
return "G{}".format(i)
def paramaterized(candidates):
"""candidates = [(args, ..., expected)]"""
def _parameterize(method):
env = sys._getframe(1).f_locals
method_name = method.__name__
for i, args in enumerate(candidates):
paramaters = args[:-1]
expected = args[-1]
def test(self, n=i):
return method(self, *candidates[n])
test.__name__ = "{}_{}".format(method_name.lstrip("_"), gensym())
doc = method.__doc__ or method_name
test.__doc__ = "{}: args={}, expected={}".format(doc, paramaters, expected)
env[test.__name__] = test
return method
return _parameterize
You did it, did not you.
Recommended Posts