[PYTHON] Let's create a function for parametrized test using frame object

Let's create a function for parametrized test using frame object

(This article didn't make it to the python advent calendar. It was full.)

What is parameterized test?

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.

Function decorator

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.

unittest test function

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.

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.

The local variable in the frame one frame above the method decorator is the position of the class

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.

Finally

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

Let's create a function for parametrized test using frame object
Let's create a virtual environment for Python
How to create a function object from a string
Let's create a REST API using SpringBoot + MongoDB
Let's make a module for Python using SWIG
Create a function in Python
Let's draw a logistic function
Let's create a function to hold down Button in Tkinter
Create a python GUI using tkinter
Create a nested dictionary using defaultdict
Create a CRUD API using FastAPI
Create a C wrapper using Boost.Python
How to divide and process a data frame using the groupby function
How to make a model for object detection using YOLO in 3 hours
Create multiple line charts from a data frame at once using Matplotlib
Create a survival prediction model for Kaggle Titanic passengers without using Python
Let's create a chat function with Vue.js + AWS Lambda + dynamo DB [AWS settings]
A memorandum of using Python's input function
Create a model for your Django schedule
Let's create a free group with Python
Create a JSON object mapper in Python
Create a graph using the Sympy module
Impressions of using Flask for a month
[Python] Create a Batch environment using AWS-CDK
Create documentation and test code using doctest.testfile ()
Let's make a multilingual site using flask-babel
Create a Hatena dictionary for SKK (additional)
Equipped with a card function using payjp
Create a dataframe from excel using pandas
Let's make a Backend plugin for Errbot
[AWS] Let's run a unit test of Lambda function in the local environment
Let's write an aggregation process for a certain period using pandas × groupby × Grouper