[PYTHON] Erstellen wir eine Funktion für den parametrisierten Test mithilfe eines Rahmenobjekts

Erstellen wir eine Funktion für den parametrisierten Test mithilfe eines Rahmenobjekts

(Dieser Artikel hat es nicht in den Python-Adventskalender geschafft. Er war voll.)

Was ist ein parametrisierter Test?

Es ist ein Test, der mit angegebenen Parametern ausgeführt wird. Ich möchte so etwas wie das Folgende tun.

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):
        """Einfacher Additionsvergleich"""
        result = self._callFUT(x, y)
        self.assertEqual(result, expected)

Das Ausführungsergebnis sieht wie folgt aus. _test_add wird 5 mal mit verschiedenen Parametern aufgerufen.

...F.
======================================================================
FAIL: test_add_G4 (__main__.Tests)
Einfacher Additionsvergleich: 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)

Es geht darum, wie man eine solche parametrisierte Funktion erstellt.

Funktionsdekorateur

Ein Funktionsdekorateur ist eine Funktion, die eine Funktion umschließt. Sie können das Verhalten der Funktion ändern, indem Sie sie einschließen. Es ist im Grunde eine Funktion, die eine Funktion zurückgibt.

Wenn die folgende Hello-Klasse definiert wurde.

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

Definieren Sie einen Doppeldekorateur wie den folgenden.

def double(method):
    def _double(self):  #Da es eine Methode ist, nimm dich selbst
        result = method(self)
        return result + result
    return _double

double ist ein Dekorateur, der das Ergebnis der internen Methode zweimal ausgibt. Wenn Sie dies früher an die Hallo-Methode der Hello-Klasse anhängen, ändert sich das Verhalten von Hello.hello ().

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 Testfunktion

unitttest.TestCase erkennt Methoden, die mit "test_" beginnen, als Testmethoden. Um es in einen parametrisierten Test umzuwandeln, müssen daher mehrere Testmethoden generiert werden.

Versuchen wir, einen Klassendekorator zu erstellen und die Anzahl der Testmethoden zu erhöhen. Der Klassendekorateur nimmt die Klasse und gibt sie zurück. Es ist eine Klassenversion des Funktionsdekorators.

import unittest


def add_foo_test(names):
    def _add_foo_test(cls):
        for name in names:
            def test(self):
                #Machen Sie einen Test, der im Text erfolgreich ist
                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 ist eine Funktion, die eine Testmethode erstellt, mit der eine SMS mit einer Liste übergebener Namen erfolgreich gesendet werden kann. Dieses Mal wurden "foo", "bar", "boo" übergeben, sodass die Testmethoden test_foo, test_bar, test_boo erstellt wurden.

Das Ergebnis ist wie folgt.

...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Sie machen drei Tests. Sollte ich mit einem parametrisierten Test etwas Ähnliches machen? Es bedeutet das.

frame object

Aber es gibt immer noch Probleme. Die parametrisierte Funktion, die ich erstellen wollte, war eine Funktion zum Dekorieren einer Methode. Was ich jedoch an der vorherigen Stelle gemacht habe, war ein Dekorateur, der die Klasse dekoriert. Es wird nicht so funktionieren wie es ist.

Es sieht so aus, als müssten Sie den Wert der Klasse über die Methode festlegen. Dies ist so etwas wie das Abrufen eines laufenden Aufrufstapels.

Stapel aufrufen

Der Aufrufstapel ist der Stapel, auf den die Umgebung während der Ausführung der Funktion geladen wird. Es enthält die Umgebung zum Zeitpunkt jedes Anrufs. (Aus diesem Grund funktioniert es auch dann, wenn Sie es rekursiv aufrufen.)

Schauen wir uns ein Beispiel an. Es gibt Funktionen f und g. Es ist wie g ruft f. f ist eine Funktion, die die lokale Variable zu diesem Zeitpunkt als Wörterbuch zurückgibt.

def f(x):
    return locals()  #Extrahieren Sie lokale Variablen in dieser Umgebung als Wörterbuch.


def g(x):
    r = f(x*x)
    print(locals())
    return r

print(g(10))
# {'r': {'x': 100}, 'x': 10}  # g
# {'x': 100} # f

Wenn ich es ausführe, wird nur x zurückgegeben.

Fügen wir eine lokale Variable von g in f hinzu. Zum Zeitpunkt des Aufrufs hat es die folgende Form, sodass Sie g von f aus sehen können, indem Sie auf den Aufrufstapel zugreifen.

[f] <-- current
[g]
[toplevel]

Sie können es berühren, indem Sie eine verdächtige Funktion namens sys._getframe verwenden. Als Argument übergeben wir die Anzahl der gewünschten Frames von der aktuellen Position.

[f] <- sys._getframe()Reinkommen
[g] <- sys._getframe(1)Reinkommen
[toplevel]

Fügen wir eine Variable namens Dummy hinzu. Ändern Sie den Code wie folgt.


import sys


def f(x):
    sys._getframe(1).f_locals["dummy"] = "*dummy*" #Fügen Sie der lokalen Variablen des obigen Frames einen Dummy hinzu
    return locals()  #Extrahieren Sie lokale Variablen in dieser Umgebung als Wörterbuch.


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

Auf diese Weise konnte ich die lokale Variable in g in f ändern.

Die lokale Variable im Frame, einen Frame über dem Methodendekorator, ist die Position der Klasse

Was war der Grund, den vorherigen Aufrufstapel zu berühren? Dies liegt daran, dass die lokale Variable im Frame ein Frame über dem Methodendekorator die Position der Klasse ist. Mit anderen Worten, indem Sie in den Aufrufstapel der parametrisierten Funktion schauen, die Sie erstellen wollten, können Sie als Methodendekorator innerhalb der Funktion auf die Klasse zugreifen, die diese Methode enthält. Lassen Sie mich Ihnen ein Beispiel geben.


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

Der Dekorator mit dem Namen add_A ist ein Methodendekorator. Sie können über den Aufrufstapel auf die Klasse zugreifen. Eine Klassenvariable namens A wird gesetzt.

Schließlich

Fassen wir die bisherigen Ergebnisse zusammen.

Sie können jetzt eine parametrisierte Funktion erstellen, indem Sie diese kombinieren. Das Folgende ist die Implementierung.

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

Du hast es getan, nicht wahr?

Recommended Posts

Erstellen wir eine Funktion für den parametrisierten Test mithilfe eines Rahmenobjekts
Erstellen wir eine virtuelle Umgebung für Python
So erstellen Sie ein Funktionsobjekt aus einer Zeichenfolge
Erstellen wir eine REST-API mit SpringBoot + MongoDB
Lassen Sie uns mit SWIG ein Modul für Python erstellen
Erstellen Sie eine Funktion in Python
Versuchen Sie, eine logistische Funktion zu zeichnen
Erstellen wir eine Funktion, um die Schaltfläche in Tkinter gedrückt zu halten
Erstellen Sie mit tkinter eine Python-GUI
Erstellen Sie ein verschachteltes Wörterbuch mit defaultdict
Erstellen Sie die CRUD-API mit der Fast API
Erstellen Sie mit Boost.Python einen C-Sprach-Wrapper
Teilen und Verarbeiten eines Datenrahmens mithilfe der Groupby-Funktion
So erstellen Sie mit YOLO in 3 Stunden ein Modell für die Objekterkennung
Verwenden Sie Matplotlib, um mehrere Liniendiagramme gleichzeitig aus einem Datenrahmen zu erstellen
Erstellen Sie ein Überlebensvorhersagemodell für Passagiere der Kaggle Titanic, ohne Python zu verwenden
Erstellen wir eine Chat-Funktion mit Vue.js + AWS Lambda + Dynamo DB [AWS-Einstellungen]
Hinweis zur Verwendung der Python-Eingabefunktion
Erstellen Sie ein Modell für Ihren Django-Zeitplan
Erstellen wir mit Python eine kostenlose Gruppe
Erstellen Sie mit dem Sympy-Modul ein Diagramm
Eindrücke von der Verwendung von Flask für einen Monat
[Python] Erstellen Sie eine Stapelumgebung mit AWS-CDK
Erstellen Sie Dokumentation und Testcode mit doctest.testfile ()
Lassen Sie uns mit flask-babel eine mehrsprachige Site erstellen
Erstellen Sie ein Hatena-Wörterbuch für SKK (zusätzlich)
Ausgestattet mit einer Kartenfunktion mit payjp
Erstellen Sie mit Pandas einen Datenrahmen aus Excel
Lassen Sie uns ein Backend-Plug-In für Errbot erstellen
[AWS] Lassen Sie uns einen Komponententest der Lambda-Funktion in der lokalen Umgebung durchführen
Schreiben wir einen Aggregationsprozess für einen bestimmten Zeitraum mit pandas × groupby × Grouper