Wie generieren Sie dynamische (parametrisierte) Unit-Tests in Python?


234

Ich habe eine Art Testdaten und möchte für jeden Artikel einen Komponententest erstellen. Meine erste Idee war es so zu machen:

import unittest

l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]

class TestSequence(unittest.TestCase):
    def testsample(self):
        for name, a,b in l:
            print "test", name
            self.assertEqual(a,b)

if __name__ == '__main__':
    unittest.main()

Der Nachteil dabei ist, dass alle Daten in einem Test verarbeitet werden. Ich möchte einen Test für jeden Artikel im laufenden Betrieb erstellen. Irgendwelche Vorschläge?



2
Ein guter Link, der eine Antwort geben kann: eli.thegreenplace.net/2014/04/02/…
gaborous

Antworten:


173

Dies wird als "Parametrisierung" bezeichnet.

Es gibt verschiedene Tools, die diesen Ansatz unterstützen. Z.B:

Der resultierende Code sieht folgendermaßen aus:

from parameterized import parameterized

class TestSequence(unittest.TestCase):
    @parameterized.expand([
        ["foo", "a", "a",],
        ["bar", "a", "b"],
        ["lee", "b", "b"],
    ])
    def test_sequence(self, name, a, b):
        self.assertEqual(a,b)

Welches wird die Tests generieren:

test_sequence_0_foo (__main__.TestSequence) ... ok
test_sequence_1_bar (__main__.TestSequence) ... FAIL
test_sequence_2_lee (__main__.TestSequence) ... ok

======================================================================
FAIL: test_sequence_1_bar (__main__.TestSequence)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/parameterized/parameterized.py", line 233, in <lambda>
    standalone_func = lambda *a: func(*(a + p.args), **p.kwargs)
  File "x.py", line 12, in test_sequence
    self.assertEqual(a,b)
AssertionError: 'a' != 'b'

Aus historischen Gründen werde ich die ursprüngliche Antwort um 2008 belassen):

Ich benutze so etwas:

import unittest

l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]

class TestSequense(unittest.TestCase):
    pass

def test_generator(a, b):
    def test(self):
        self.assertEqual(a,b)
    return test

if __name__ == '__main__':
    for t in l:
        test_name = 'test_%s' % t[0]
        test = test_generator(t[1], t[2])
        setattr(TestSequense, test_name, test)
    unittest.main()

24
Eigentlich generiert dieser Code für jeden Test einen anderen Namen (sonst würde er eigentlich nicht funktionieren). In dem angegebenen Beispiel werden die ausgeführten Tests mit "test_foo", "test_bar" bzw. "test_lee" bezeichnet. So bleibt der von Ihnen erwähnte Vorteil (und er ist ein großer) erhalten, solange Sie vernünftige Namen generieren.
Toji

1
Wie die Antwort von @codeape besagt, behandelt die Nase dies. Die Nase scheint jedoch nicht mit Unicode umzugehen. Daher ist dies für mich eine bevorzugte Lösung. +1
Keith Pinson

5
Beachten Sie also, dass die doppelte Frage die richtige Antwort enthält : stackoverflow.com/a/2799009/322020 - Sie müssen verwenden, .__name__ =um das .exact_methodTesten zu aktivieren
Nakilon

7
Warum wird der Code, der die Klasse ändert, in der if __name__ == '__main__'Bedingung angezeigt? Sicherlich sollte es darüber
hinausgehen

4
Ich denke nicht, dass dies eine gute Lösung ist. Der Code eines Unittest sollte nicht davon abhängen, wie er aufgerufen wird. Der TestCase sollte in der Nase oder im Pytest oder in einer anderen Testumgebung verwendbar sein.
Guettli

146

Verwenden von unittest (seit 3.4)

Seit Python 3.4 verfügt das Standardbibliothekspaket unittestüber den subTestKontextmanager.

Siehe die Dokumentation:

Beispiel:

from unittest import TestCase

param_list = [('a', 'a'), ('a', 'b'), ('b', 'b')]

class TestDemonstrateSubtest(TestCase):
    def test_works_as_expected(self):
        for p1, p2 in param_list:
            with self.subTest():
                self.assertEqual(p1, p2)

Sie können auch eine benutzerdefinierte Nachricht und Parameterwerte angeben, um subTest():

with self.subTest(msg="Checking if p1 equals p2", p1=p1, p2=p2):

Mit der Nase

Die Nase - Test - Framework unterstützt dies .

Beispiel (der folgende Code ist der gesamte Inhalt der Datei, die den Test enthält):

param_list = [('a', 'a'), ('a', 'b'), ('b', 'b')]

def test_generator():
    for params in param_list:
        yield check_em, params[0], params[1]

def check_em(a, b):
    assert a == b

Die Ausgabe des Befehls nosetests:

> nosetests -v
testgen.test_generator('a', 'a') ... ok
testgen.test_generator('a', 'b') ... FAIL
testgen.test_generator('b', 'b') ... ok

======================================================================
FAIL: testgen.test_generator('a', 'b')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python2.5/site-packages/nose-0.10.1-py2.5.egg/nose/case.py", line 203, in runTest
    self.test(*self.arg)
  File "testgen.py", line 7, in check_em
    assert a == b
AssertionError

----------------------------------------------------------------------
Ran 3 tests in 0.006s

FAILED (failures=1)

3
Dies ist eine sehr saubere Methode, um Testfälle dynamisch zu generieren.
gaborous

Beachten Sie jedoch, dass 'setup ()' nicht weiß, welche Variablen als Argumente verwendet werden. Tatsächlich weiß setup () nicht, welcher Test ausgeführt wird, oder vars wird in test_generator () festgelegt. Dies erschwert die Überprüfung der Integrität in setup () und ist einer der Gründe, warum einige Leute py.test bevorzugen.
Scott Prive

1
Upvoted für den Update-Bereich. Genau das, was ich brauchte. :)
Saurabh Shrivastava

1
Gibt es eine Möglichkeit, die unittest-Version mit pytest auszuführen, damit alle Fälle ausgeführt werden und nicht beim ersten fehlgeschlagenen Parameter angehalten wird?
Kakk11

1
Wie von @ kakk11 erwähnt, funktioniert diese Antwort (und der Untertest im Allgemeinen) nicht mit pytest. Dies ist ein bekanntes Problem. Es gibt ein aktiv entwickeltes Plugin, damit dies funktioniert: github.com/pytest-dev/pytest-subtests
Jérémie

76

Dies kann elegant mit Metaclasses gelöst werden:

import unittest

l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]

class TestSequenceMeta(type):
    def __new__(mcs, name, bases, dict):

        def gen_test(a, b):
            def test(self):
                self.assertEqual(a, b)
            return test

        for tname, a, b in l:
            test_name = "test_%s" % tname
            dict[test_name] = gen_test(a,b)
        return type.__new__(mcs, name, bases, dict)

class TestSequence(unittest.TestCase):
    __metaclass__ = TestSequenceMeta

if __name__ == '__main__':
    unittest.main()

1
Das hat bei Selenium großartig funktioniert. Als Hinweis können Sie in der Klasse TestSequence "statische" Methoden wie setUp (self), is_element_present (self, wie, was), ... tearDown (self) definieren. Setzen sie nach der „ metaclass = TestSequenceMeta“ Erklärung scheint zu funktionieren.
Liebe und Frieden - Joe Codeswell

5
Diese Lösung ist besser als die, die meiner Meinung nach als akzeptiert ausgewählt wurde.
Petroslamb

2
@petroslamb Die __new__Methode in der Metaklasse wird aufgerufen, wenn die Klasse selbst definiert wird, nicht wenn die erste Instanz erstellt wird. Ich würde mir vorstellen, dass diese Methode zum dynamischen Erstellen von Testmethoden besser mit der Selbstbeobachtung kompatibel ist, mit der unittestbestimmt wird, wie viele Tests sich in einer Klasse befinden (dh sie kann die Liste der Tests zusammenstellen, bevor jemals eine Instanz dieser Klasse erstellt wird).
BillyBBone

11
Hinweis: Ändern Sie dies in Python 3 in:class TestSequence(unittest.TestCase, metaclass=TestSequenceMeta):[...]
Mathieu_Du

3
Könnten Sie bitte dctanstelle von verwenden dict? Die Verwendung von Schlüsselwörtern als Variablennamen ist verwirrend und fehleranfällig.
npfoss

49

Ab Python 3.4 wurden zu diesem Zweck Untertests für unittest eingeführt. Einzelheiten finden Sie in der Dokumentation . TestCase.subTest ist ein Kontextmanager, mit dem Asserts in einem Test isoliert werden können, sodass ein Fehler mit Parameterinformationen gemeldet wird, die Testausführung jedoch nicht gestoppt wird. Hier ist das Beispiel aus der Dokumentation:

class NumbersTest(unittest.TestCase):

def test_even(self):
    """
    Test that numbers between 0 and 5 are all even.
    """
    for i in range(0, 6):
        with self.subTest(i=i):
            self.assertEqual(i % 2, 0)

Die Ausgabe eines Testlaufs wäre:

======================================================================
FAIL: test_even (__main__.NumbersTest) (i=1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "subtests.py", line 32, in test_even
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_even (__main__.NumbersTest) (i=3)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "subtests.py", line 32, in test_even
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_even (__main__.NumbersTest) (i=5)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "subtests.py", line 32, in test_even
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

Dies ist auch Teil von unittest2 , sodass es für frühere Versionen von Python verfügbar ist.


1
Die beste Lösung, wenn Sie Python 3.4 und höher verwenden.
Max Malysh

4
Mit unittest2 ist dies auch für Python 2.7 verfügbar.
Bernhard

11
Ein wesentlicher Unterschied zwischen diesem Ansatz und separaten Tests besteht darin, dass der Teststatus nicht jedes Mal zurückgesetzt wird. (Das heißt, setUp()und tearDown()werden nicht zwischen den Untertests ausgeführt.)
Kevin Christopher Henry

1
@ KevinChristopherHenry Ja, self.setUp()kann aber theoretisch manuell aus dem Subtest heraus aufgerufen werden. Was tearDownautomatisch am Ende könnte ausreichen, nannte es zu haben.
Acumenus

Ich denke, dass dies in Verbindung mit dem oben genannten Metaklassen-Ansatz sehr effektiv sein könnte.
Nathan Chappell

36

load_tests ist ein wenig bekannter Mechanismus, der in 2.7 eingeführt wurde, um eine TestSuite dynamisch zu erstellen. Mit ihm können Sie einfach parametrisierte Tests erstellen.

Beispielsweise:

import unittest

class GeneralTestCase(unittest.TestCase):
    def __init__(self, methodName, param1=None, param2=None):
        super(GeneralTestCase, self).__init__(methodName)

        self.param1 = param1
        self.param2 = param2

    def runTest(self):
        pass  # Test that depends on param 1 and 2.


def load_tests(loader, tests, pattern):
    test_cases = unittest.TestSuite()
    for p1, p2 in [(1, 2), (3, 4)]:
        test_cases.addTest(GeneralTestCase('runTest', p1, p2))
    return test_cases

Dieser Code führt alle Testfälle in der TestSuite aus, die von load_tests zurückgegeben werden. Der Erkennungsmechanismus führt keine anderen Tests automatisch aus.

Alternativ können Sie auch die Vererbung verwenden, wie in diesem Ticket gezeigt: http://bugs.python.org/msg151444


1
Der obige Code schlägt fehl: TypeError: __init __ () akzeptiert höchstens 2 Argumente (4 angegeben)
maximal

2
Den zusätzlichen Konstruktorparametern wurden Null-Standardwerte hinzugefügt.
Javier

Ich bevorzuge den Code zur Nasenparametrisierung in der Antwort von @ mojo , aber für meine Kunden ist es einfach zu nützlich, eine zusätzliche Abhängigkeit zu vermeiden, sodass ich diesen für sie verwenden werde.
Salbei

1
Diese Lösung war mein Favorit auf dieser Seite. Sowohl Nose , wie in der aktuellen Top-Antwort vorgeschlagen, als auch die Gabel Nose2 dienen nur der Wartung, und letztere schlägt vor, dass Benutzer stattdessen Pytest versuchen . Was für ein Durcheinander - ich bleibe bei einem nativen Ansatz wie diesem!
Sean

1
Bonus: Fähigkeit, die shortDescription-Methode für die in params
übergebene

33

Dies kann mit pytest erfolgen . Schreiben Sie einfach die Datei test_me.pymit Inhalt:

import pytest

@pytest.mark.parametrize('name, left, right', [['foo', 'a', 'a'],
                                               ['bar', 'a', 'b'],
                                               ['baz', 'b', 'b']])
def test_me(name, left, right):
    assert left == right, name

Und führen Sie Ihren Test mit Befehl aus py.test --tb=short test_me.py. Dann sieht die Ausgabe folgendermaßen aus:

=========================== test session starts ============================
platform darwin -- Python 2.7.6 -- py-1.4.23 -- pytest-2.6.1
collected 3 items

test_me.py .F.

================================= FAILURES =================================
_____________________________ test_me[bar-a-b] _____________________________
test_me.py:8: in test_me
    assert left == right, name
E   AssertionError: bar
==================== 1 failed, 2 passed in 0.01 seconds ====================

Es ist einfach!. Auch pytest hat mehr Funktionen wie fixtures, mark, assert, etc ...


1
Ich suchte nach einem einfachen, einfachen Beispiel für die Parametrisierung von Testfällen mit py.test. Vielen Dank!
Timgeb

@ Timgeb Ich bin froh, Ihnen zu helfen. Überprüfen py.test Tag, für weitere Beispiele. Außerdem schlage ich vor, Hamcrest zu verwenden, um Ihren Asserts mit lesbaren Mutchern etwas Zucker hinzuzufügen, die auf Ihre eigene Weise modifiziert, kombiniert oder hergestellt werden können. Außerdem haben wir Allure-Python , eine gut aussehende Berichtsgenerierung fürpy.test
Sergey Voronezhskiy

Vielen Dank. Ich habe gerade angefangen, von unittestpy.test zu wechseln. Früher hatte ich TestCaseBasisklassen, die in der Lage waren, Kinder mit verschiedenen Argumenten dynamisch zu erstellen, die sie als Klassenvariablen speichern würden ... was etwas unhandlich war.
Timgeb

1
@ Timgeb Ja, du hast recht. Das Killer-Feature von py.testist Yield_Fixtures . Was das Setup tun kann , einige nützliche Daten in den Test zurückgeben und nach Testende den Abriss durchführen . Vorrichtungen können auch parametirisiert werden .
Sergey Voronezhskiy

12

Verwenden Sie die ddt- Bibliothek. Es werden einfache Dekoratoren für die Testmethoden hinzugefügt:

import unittest
from ddt import ddt, data
from mycode import larger_than_two

@ddt
class FooTestCase(unittest.TestCase):

    @data(3, 4, 12, 23)
    def test_larger_than_two(self, value):
        self.assertTrue(larger_than_two(value))

    @data(1, -3, 2, 0)
    def test_not_larger_than_two(self, value):
        self.assertFalse(larger_than_two(value))

Diese Bibliothek kann mit installiert werden pip. Es ist nicht erforderlich noseund funktioniert hervorragend mit dem Standardbibliotheksmodul unittest.


6

Sie würden von der TestScenarios- Bibliothek profitieren .

testscenarios bietet eine saubere Abhängigkeitsinjektion für Python-Tests im unittest-Stil. Dies kann für Schnittstellentests (Testen vieler Implementierungen über eine einzige Testsuite) oder für die klassische Abhängigkeitsinjektion (Bereitstellen von Tests mit Abhängigkeiten außerhalb des Testcodes selbst verwendet werden, um ein einfaches Testen in verschiedenen Situationen zu ermöglichen).



4

Sie können das Nose-Ittr- Plugin ( pip install nose-ittr) verwenden.

Die Integration in vorhandene Tests ist sehr einfach. Es sind nur minimale Änderungen (falls vorhanden) erforderlich. Es unterstützt auch das Nasen- Multiprocessing-Plugin.

Nicht, dass Sie auch eine Anpassungsfunktion setuppro Test haben können.

@ittr(number=[1, 2, 3, 4])   
def test_even(self):   
    assert_equal(self.number % 2, 0)

Es ist auch möglich, nosetestParameter wie mit ihrem eingebauten Plugin zu übergeben attrib. Auf diese Weise können Sie nur einen bestimmten Test mit bestimmten Parametern ausführen:

nosetest -a number=2

Ich mag diesen Ansatz, insbesondere die pro Methode unterstützte Ebene.
Matt

3

Ich benutze Metaklassen und Dekorateure, um Tests zu generieren. Sie können meine Implementierung python_wrap_cases überprüfen . Diese Bibliothek benötigt keine Test-Frameworks.

Ihr Beispiel:

import unittest
from python_wrap_cases import wrap_case


@wrap_case
class TestSequence(unittest.TestCase):

    @wrap_case("foo", "a", "a")
    @wrap_case("bar", "a", "b")
    @wrap_case("lee", "b", "b")
    def testsample(self, name, a, b):
        print "test", name
        self.assertEqual(a, b)

Konsolenausgabe:

testsample_u'bar'_u'a'_u'b' (tests.example.test_stackoverflow.TestSequence) ... test bar
FAIL
testsample_u'foo'_u'a'_u'a' (tests.example.test_stackoverflow.TestSequence) ... test foo
ok
testsample_u'lee'_u'b'_u'b' (tests.example.test_stackoverflow.TestSequence) ... test lee
ok

Sie können auch Generatoren verwenden . Beispielsweise generiert dieser Code alle möglichen Kombinationen von Tests mit Argumenten a__listundb__list

import unittest
from python_wrap_cases import wrap_case


@wrap_case
class TestSequence(unittest.TestCase):

    @wrap_case(a__list=["a", "b"], b__list=["a", "b"])
    def testsample(self, a, b):
        self.assertEqual(a, b)

Konsolenausgabe:

testsample_a(u'a')_b(u'a') (tests.example.test_stackoverflow.TestSequence) ... ok
testsample_a(u'a')_b(u'b') (tests.example.test_stackoverflow.TestSequence) ... FAIL
testsample_a(u'b')_b(u'a') (tests.example.test_stackoverflow.TestSequence) ... FAIL
testsample_a(u'b')_b(u'b') (tests.example.test_stackoverflow.TestSequence) ... ok

2

Ich bin neulich auf ParamUnittest gestoßen, als ich mir den Quellcode für Radon angesehen habe ( Beispiel für die Verwendung im Github-Repo ). Es sollte mit anderen Frameworks funktionieren, die TestCase erweitern (wie Nose).

Hier ist ein Beispiel:

import unittest
import paramunittest


@paramunittest.parametrized(
    ('1', '2'),
    #(4, 3),    <---- uncomment to have a failing test
    ('2', '3'),
    (('4', ), {'b': '5'}),
    ((), {'a': 5, 'b': 6}),
    {'a': 5, 'b': 6},
)
class TestBar(TestCase):
    def setParameters(self, a, b):
        self.a = a
        self.b = b

    def testLess(self):
        self.assertLess(self.a, self.b)

2
import unittest

def generator(test_class, a, b):
    def test(self):
        self.assertEqual(a, b)
    return test

def add_test_methods(test_class):
    #First element of list is variable "a", then variable "b", then name of test case that will be used as suffix.
    test_list = [[2,3, 'one'], [5,5, 'two'], [0,0, 'three']]
    for case in test_list:
        test = generator(test_class, case[0], case[1])
        setattr(test_class, "test_%s" % case[2], test)


class TestAuto(unittest.TestCase):
    def setUp(self):
        print 'Setup'
        pass

    def tearDown(self):
        print 'TearDown'
        pass

_add_test_methods(TestAuto)  # It's better to start with underscore so it is not detected as a test itself

if __name__ == '__main__':
    unittest.main(verbosity=1)

ERGEBNIS:

>>> 
Setup
FTearDown
Setup
TearDown
.Setup
TearDown
.
======================================================================
FAIL: test_one (__main__.TestAuto)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/inchowar/Desktop/PyTrash/test_auto_3.py", line 5, in test
    self.assertEqual(a, b)
AssertionError: 2 != 3

----------------------------------------------------------------------
Ran 3 tests in 0.019s

FAILED (failures=1)

1
Kleines Problem mit Ihrer def add_test_methodsFunktion. Sollte def _add_test_methods ich denken
Raychaser

@ Raychaser ... Sie haben Recht ... Ich habe das behoben, aber hier nicht aktualisiert ... Danke, dass Sie das verstanden haben.
Arindam Roychowdhury

1

Verwenden Sie einfach Metaklassen, wie hier gezeigt;

class DocTestMeta(type):
    """
    Test functions are generated in metaclass due to the way some
    test loaders work. For example, setupClass() won't get called
    unless there are other existing test methods, and will also
    prevent unit test loader logic being called before the test
    methods have been defined.
    """
    def __init__(self, name, bases, attrs):
        super(DocTestMeta, self).__init__(name, bases, attrs)

    def __new__(cls, name, bases, attrs):
        def func(self):
            """Inner test method goes here"""
            self.assertTrue(1)

        func.__name__ = 'test_sample'
        attrs[func.__name__] = func
        return super(DocTestMeta, cls).__new__(cls, name, bases, attrs)

class ExampleTestCase(TestCase):
    """Our example test case, with no methods defined"""
    __metaclass__ = DocTestMeta

Ausgabe:

test_sample (ExampleTestCase) ... OK

1

Sie können TestSuitebenutzerdefinierte TestCaseKlassen verwenden.

import unittest

class CustomTest(unittest.TestCase):
    def __init__(self, name, a, b):
        super().__init__()
        self.name = name
        self.a = a
        self.b = b

    def runTest(self):
        print("test", self.name)
        self.assertEqual(self.a, self.b)

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTest(CustomTest("Foo", 1337, 1337))
    suite.addTest(CustomTest("Bar", 0xDEAD, 0xC0DE))
    unittest.TextTestRunner().run(suite)

Während die TestSuite funktioniert, werden die Argumente nicht an __init__function weitergegeben.
Jadelord

1

Ich habe festgestellt, dass dies für meine Zwecke gut funktioniert, insbesondere wenn ich Tests generieren muss, die geringfügig unterschiedliche Prozesse für eine Datensammlung ausführen.

import unittest

def rename(newName):
    def renamingFunc(func):
        func.__name__ == newName
        return func
    return renamingFunc

class TestGenerator(unittest.TestCase):

    TEST_DATA = {}

    @classmethod
    def generateTests(cls):
        for dataName, dataValue in TestGenerator.TEST_DATA:
            for func in cls.getTests(dataName, dataValue):
                setattr(cls, "test_{:s}_{:s}".format(func.__name__, dataName), func)

    @classmethod
    def getTests(cls):
        raise(NotImplementedError("This must be implemented"))

class TestCluster(TestGenerator):

    TEST_CASES = []

    @staticmethod
    def getTests(dataName, dataValue):

        def makeTest(case):

            @rename("{:s}".format(case["name"]))
            def test(self):
                # Do things with self, case, data
                pass

            return test

        return [makeTest(c) for c in TestCluster.TEST_CASES]

TestCluster.generateTests()

Die TestGeneratorKlasse kann verwendet werden, um verschiedene Sätze von Testfällen wie zu erzeugen TestCluster.

TestClusterkann als Implementierung der TestGeneratorSchnittstelle betrachtet werden.


1

Diese Lösung funktioniert mit unittestund nosefür Python 2 und Python 3:

#!/usr/bin/env python
import unittest

def make_function(description, a, b):
    def ghost(self):
        self.assertEqual(a, b, description)
    print(description)
    ghost.__name__ = 'test_{0}'.format(description)
    return ghost


class TestsContainer(unittest.TestCase):
    pass

testsmap = {
    'foo': [1, 1],
    'bar': [1, 2],
    'baz': [5, 5]}

def generator():
    for name, params in testsmap.iteritems():
        test_func = make_function(name, params[0], params[1])
        setattr(TestsContainer, 'test_{0}'.format(name), test_func)

generator()

if __name__ == '__main__':
    unittest.main()

Vielen Dank an guillaume-jacquenot für die aktualisierte Version <3!
Mopp

0

Ich hatte Probleme mit einem ganz bestimmten Stil parametrisierter Tests. Alle unsere Selenium-Tests können lokal ausgeführt werden, sie sollten jedoch auch remote auf mehreren Plattformen in SauceLabs ausgeführt werden können. Grundsätzlich wollte ich eine große Anzahl bereits geschriebener Testfälle mit möglichst wenigen Codeänderungen parametrisieren. Außerdem musste ich in der Lage sein, die Parameter an die setUp-Methode zu übergeben, für die ich anderswo keine Lösungen gesehen habe.

Folgendes habe ich mir ausgedacht:

import inspect
import types

test_platforms = [
    {'browserName': "internet explorer", 'platform': "Windows 7", 'version': "10.0"},
    {'browserName': "internet explorer", 'platform': "Windows 7", 'version': "11.0"},
    {'browserName': "firefox", 'platform': "Linux", 'version': "43.0"},
]


def sauce_labs():
    def wrapper(cls):
        return test_on_platforms(cls)
    return wrapper


def test_on_platforms(base_class):
    for name, function in inspect.getmembers(base_class, inspect.isfunction):
        if name.startswith('test_'):
            for platform in test_platforms:
                new_name = '_'.join(list([name, ''.join(platform['browserName'].title().split()), platform['version']]))
                new_function = types.FunctionType(function.__code__, function.__globals__, new_name,
                                                  function.__defaults__, function.__closure__)
                setattr(new_function, 'platform', platform)
                setattr(base_class, new_name, new_function)
            delattr(base_class, name)

    return base_class

Alles, was ich tun musste, war, jedem normalen alten TestCase einen einfachen Dekorator @sauce_labs () hinzuzufügen, und jetzt, wenn sie ausgeführt werden, werden sie verpackt und neu geschrieben, sodass alle Testmethoden parametrisiert und umbenannt werden. LoginTests.test_login (self) wird ausgeführt als LoginTests.test_login_internet_explorer_10.0 (self), LoginTests.test_login_internet_explorer_11.0 (self) und LoginTests.test_login_firefox_43.0 (self), und jeder hat den Parameter self.platform zu entscheiden Plattform, auf der auch in LoginTests.setUp ausgeführt werden kann, was für meine Aufgabe von entscheidender Bedeutung ist, da dort die Verbindung zu SauceLabs initialisiert wird.

Wie auch immer, ich hoffe, dies könnte jemandem helfen, der eine ähnliche "globale" Parametrisierung seiner Tests durchführen möchte!


0

Die metaklassenbasierten Antworten funktionieren weiterhin in Python3, aber anstelle des __metaclass__Attributs muss der metaclassParameter wie folgt verwendet werden:

class ExampleTestCase(TestCase,metaclass=DocTestMeta):
    pass

0

Meta-Programmierung macht Spaß, kann aber auf dem Weg sein. Die meisten Lösungen hier machen es schwierig:

  • Starten Sie selektiv einen Test
  • Zeigen Sie zurück auf den Code, der dem Namen des Tests gegeben wurde

Mein erster Vorschlag ist also, dem einfachen / expliziten Pfad zu folgen (funktioniert mit jedem Testläufer):

import unittest

class TestSequence(unittest.TestCase):

    def _test_complex_property(self, a, b):
        self.assertEqual(a,b)

    def test_foo(self):
        self._test_complex_property("a", "a")
    def test_bar(self):
        self._test_complex_property("a", "b")
    def test_lee(self):
        self._test_complex_property("b", "b")

if __name__ == '__main__':
    unittest.main()

Da wir uns nicht wiederholen sollten, baut mein zweiter Vorschlag auf der Antwort von @ Javier auf: Umfassen Sie eigenschaftsbasierte Tests. Hypothesenbibliothek:

  • ist "unerbittlicher in Bezug auf die Erstellung von Testfällen als wir Menschen"
  • wird einfache Zählbeispiele liefern
  • funktioniert mit jedem Testläufer
  • hat viele weitere interessante Funktionen (Statistiken, zusätzliche Testausgabe, ...)

    Klasse TestSequence (unittest.TestCase):

    @given(st.text(), st.text())
    def test_complex_property(self, a, b):
        self.assertEqual(a,b)

Um Ihre spezifischen Beispiele zu testen, fügen Sie einfach hinzu:

    @example("a", "a")
    @example("a", "b")
    @example("b", "b")

Um nur ein bestimmtes Beispiel auszuführen, können Sie die anderen Beispiele auskommentieren (vorausgesetzt, das Beispiel wird zuerst ausgeführt). Möglicherweise möchten Sie verwenden @given(st.nothing()). Eine andere Möglichkeit besteht darin, den gesamten Block durch Folgendes zu ersetzen:

    @given(st.just("a"), st.just("b"))

Ok, Sie haben keine eindeutigen Testnamen. Aber vielleicht brauchen Sie nur:

  • ein beschreibender Name der zu testenden Eigenschaft.
  • Welche Eingabe führt zu einem Fehler (fälschendes Beispiel).

Lustigeres Beispiel


0

Super spät zur Party, aber ich hatte Probleme damit, dass diese funktionieren setUpClass .

Hier ist eine Version von @ Javiers Antwort , die den setUpClassZugriff auf dynamisch zugewiesene Attribute ermöglicht.

import unittest


class GeneralTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print ''
        print cls.p1
        print cls.p2

    def runTest1(self):
        self.assertTrue((self.p2 - self.p1) == 1)

    def runTest2(self):
        self.assertFalse((self.p2 - self.p1) == 2)


def load_tests(loader, tests, pattern):
    test_cases = unittest.TestSuite()
    for p1, p2 in [(1, 2), (3, 4)]:
        clsname = 'TestCase_{}_{}'.format(p1, p2)
        dct = {
            'p1': p1,
            'p2': p2,
        }
        cls = type(clsname, (GeneralTestCase,), dct)
        test_cases.addTest(cls('runTest1'))
        test_cases.addTest(cls('runTest2'))
    return test_cases

Ausgänge

1
2
..
3
4
..
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

0

Nur um eine andere Lösung in die Mischung zu werfen;)

Dies ist praktisch das gleiche wie parameterizedoben erwähnt, jedoch spezifisch für unittest:

def sub_test(param_list):
    """Decorates a test case to run it as a set of subtests."""

    def decorator(f):

        @functools.wraps(f)
        def wrapped(self):
            for param in param_list:
                with self.subTest(**param):
                    f(self, **param)

        return wrapped

    return decorator

Anwendungsbeispiel:

class TestStuff(unittest.TestCase):
    @sub_test([
        dict(arg1='a', arg2='b'),
        dict(arg1='x', arg2='y'),
    ])
    def test_stuff(self, a, b):
        ...

-1

Neben setattr können wir seit Python 3.2 load_tests verwenden. Bitte lesen Sie den Blog-Beitrag blog.livreuro.com/de/coding/python/how-to-generate-discoverable-unit-tests-in-python-dynamically/

class Test(unittest.TestCase):
    pass

def _test(self, file_name):
    open(file_name, 'r') as f:
        self.assertEqual('test result',f.read())

def _generate_test(file_name):
    def test(self):
        _test(self, file_name)
    return test

def _generate_tests():
    for file in files:
        file_name = os.path.splitext(os.path.basename(file))[0]
        setattr(Test, 'test_%s' % file_name, _generate_test(file))

test_cases = (Test,)

def load_tests(loader, tests, pattern):
    _generate_tests()
    suite = TestSuite()
    for test_class in test_cases:
        tests = loader.loadTestsFromTestCase(test_class)
        suite.addTests(tests)
    return suite

if __name__ == '__main__':
    _generate_tests()
    unittest.main()

-1

Das Folgende ist meine Lösung. Ich finde dies nützlich, wenn: 1. für unittest funktionieren sollte. Testfall und unittest entdecken 2. Eine Reihe von Tests für verschiedene Parametereinstellungen ausführen lassen. 3. Sehr einfach, keine Abhängigkeit von anderen Paketen importieren unittest

    class BaseClass(unittest.TestCase):
        def setUp(self):
            self.param = 2
            self.base = 2

        def test_me(self):
            self.assertGreaterEqual(5, self.param+self.base)

        def test_me_too(self):
            self.assertLessEqual(3, self.param+self.base)



     class Child_One(BaseClass):
        def setUp(self):
            BaseClass.setUp(self)
            self.param = 4


     class Child_Two(BaseClass):
        def setUp(self):
            BaseClass.setUp(self)
            self.param = 1

Dies beantwortet nicht die Frage, bei der es darum geht, Tests im laufenden Betrieb zu generieren.
Lenz

-1
import unittest

def generator(test_class, a, b,c,d,name):
    def test(self):
        print('Testexecution=',name)
        print('a=',a)
        print('b=',b)
        print('c=',c)
        print('d=',d)

    return test

def add_test_methods(test_class):
    test_list = [[3,3,5,6, 'one'], [5,5,8,9, 'two'], [0,0,5,6, 'three'],[0,0,2,3,'Four']]
    for case in test_list:
        print('case=',case[0], case[1],case[2],case[3],case[4])
        test = generator(test_class, case[0], case[1],case[2],case[3],case[4])
        setattr(test_class, "test_%s" % case[4], test)


class TestAuto(unittest.TestCase):
    def setUp(self):
        print ('Setup')
        pass

    def tearDown(self):
        print ('TearDown')
        pass

add_test_methods(TestAuto)

if __name__ == '__main__':
    unittest.main(verbosity=1)

Anscheinend haben Sie dort die Formatierung verloren. es ist wirklich schwer zu lesen, wie es steht
Arturo
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.