2009-11-04 20 views
26

Mögliche Duplizieren:
How to generate dynamic (parametrized) unit tests in python?Schreibe eine wiederverwendbare (parametrisiert) unittest.TestCase Methode

Ich schreibe Tests, um die Unittest-Paket mit, und ich möchte wiederholt Code vermeiden . Ich werde eine Reihe von Tests durchführen, die alle eine sehr ähnliche Methode erfordern, aber mit jeweils nur einem Wert. Ein stark vereinfacht und nutzlos Beispiel wäre:

class ExampleTestCase(unittest.TestCase): 

    def test_1(self): 
     self.assertEqual(self.somevalue, 1) 

    def test_2(self): 
     self.assertEqual(self.somevalue, 2) 

    def test_3(self): 
     self.assertEqual(self.somevalue, 3) 

    def test_4(self): 
     self.assertEqual(self.somevalue, 4) 

Gibt es eine Möglichkeit das obige Beispiel zu schreiben, ohne die gesamten Code jedes Mal zu wiederholen, sondern eine allgemeine Methode zu schreiben, zum Beispiel

def test_n(self, n): 
     self.assertEqual(self.somevalue, n) 

und Unittest sagen, diesen Test mit verschiedenen Eingaben zu versuchen?

+0

Haben Sie einen Weg finden, dies zu tun? Oder haben Sie vielleicht ein anderes Werkzeug für diese Aufgabe gefunden? Ich brauche auch solches Verhalten. – legesh

+0

http://thebongtraveller.blogspot.sg/2015/12/art-of-unittest-writing-auto-generation.html Ist das die gleiche Sache? –

Antwort

0

Vielleicht so etwas wie:

def test_many(self): 
    for n in range(0,1000): 
     self.assertEqual(self.somevalue, n) 
+1

Dies ist nicht was ich suche, weil es stoppt, sobald einer der Tests fehlschlägt. Ich suche nach einer Lösung, bei der ein Testversagen die Ausführung der anderen nicht verhindert. – astrofrog

+0

@Morgoth: Warum? Warum sollten Sie mehr Tests durchführen, wenn Sie wissen, dass Sie einen Fehler haben? –

+3

Da es nichts zu sagen gibt, werden auch die anderen Tests scheitern. Es ist wichtig zu wissen, ob alle Tests fehlschlagen, oder nur eins oder zwei, da dies helfen kann, das Problem zu diagnostizieren. Es ist schön, von Anfang an genau zu wissen, wie viele Fehler Sie haben, Sie müssen sie nicht nacheinander beheben, bis sie aufhören. – astrofrog

1

denke ich, was Sie wollen, ist "parametrisierte Tests".

Ich glaube nicht, Unittest-Modul unterstützt dies (leider), aber wenn ich diese Funktion fügten würde es in etwa so aussehen:

# Will run the test for all combinations of parameters 
@RunTestWith(x=[0, 1, 2, 3], y=[-1, 0, 1]) 
def testMultiplication(self, x, y): 
    self.assertEqual(multiplication.multiply(x, y), x*y) 

Mit dem bestehenden Unittest-Modul, ein einfaches Dekorateur wie diese Ich bin nicht in der Lage, den Test mehrmals zu "replizieren", aber ich denke, dass dies mit einer Kombination aus einem Dekorator und einer Metaklasse möglich ist (Metaklasse sollte alle 'Test *' - Methoden beachten und (unter verschiedenen automatisch generierten Namen) replizieren die einen Dekorateur angewendet haben).

3

Wenn Sie wirklich mehrere Einheiten haben möchten, dann brauchen Sie mehrere Methoden. Der einzige Weg, um das zu erreichen, ist durch eine Art Code-Generierung. Sie können dies über Metaklassen tun, oder indem Sie die Klasse nach der Definition optimieren, einschließlich (wenn Sie Python 2.6 verwenden) über einen Klassendekorator.

Hier ist eine Lösung, die nach den speziellen 'multititest' und 'multitest_values' Mitgliedern sucht und diese verwendet, um die Testmethoden on the fly zu erstellen. Nicht elegant, aber es hat in etwa, was Sie wollen:

import unittest 
import inspect 

class SomeValue(object): 
    def __eq__(self, other): 
     return other in [1, 3, 4] 

class ExampleTestCase(unittest.TestCase): 
    somevalue = SomeValue() 

    multitest_values = [1, 2, 3, 4] 
    def multitest(self, n): 
     self.assertEqual(self.somevalue, n) 

    multitest_gt_values = "ABCDEF" 
    def multitest_gt(self, c): 
     self.assertTrue(c > "B", c) 


def add_test_cases(cls): 
    values = {} 
    functions = {} 
    # Find all the 'multitest*' functions and 
    # matching list of test values. 
    for key, value in inspect.getmembers(cls): 
     if key.startswith("multitest"): 
      if key.endswith("_values"): 
       values[key[:-7]] = value 
      else: 
       functions[key] = value 

    # Put them together to make a list of new test functions. 
    # One test function for each value 
    for key in functions: 
     if key in values: 
      function = functions[key] 
      for i, value in enumerate(values[key]): 
       def test_function(self, function=function, value=value): 
        function(self, value) 
       name ="test%s_%d" % (key[9:], i+1) 
       test_function.__name__ = name 
       setattr(cls, name, test_function) 

add_test_cases(ExampleTestCase) 

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

Dies ist die Ausgabe aus, wenn ich es laufen

% python stackoverflow.py 
.F..FF.... 
====================================================================== 
FAIL: test_2 (__main__.ExampleTestCase) 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
    File "stackoverflow.py", line 34, in test_function 
    function(self, value) 
    File "stackoverflow.py", line 13, in multitest 
    self.assertEqual(self.somevalue, n) 
AssertionError: <__main__.SomeValue object at 0xd9870> != 2 

====================================================================== 
FAIL: test_gt_1 (__main__.ExampleTestCase) 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
    File "stackoverflow.py", line 34, in test_function 
    function(self, value) 
    File "stackoverflow.py", line 17, in multitest_gt 
    self.assertTrue(c > "B", c) 
AssertionError: A 

====================================================================== 
FAIL: test_gt_2 (__main__.ExampleTestCase) 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
    File "stackoverflow.py", line 34, in test_function 
    function(self, value) 
    File "stackoverflow.py", line 17, in multitest_gt 
    self.assertTrue(c > "B", c) 
AssertionError: B 

---------------------------------------------------------------------- 
Ran 10 tests in 0.001s 

FAILED (failures=3) 

Sie sofort einige der Probleme sehen, die mit Code-Generierung auftreten. Woher kommt "test_gt_1"? Ich könnte den Namen in den längeren "test_multitest_gt_1" ändern, aber welcher Test ist dann 1? Besser wäre es, von _0 statt von _1 zu starten, und vielleicht wissen Sie in Ihrem Fall, dass die Werte als Python-Funktionsname verwendet werden können.

Ich mag diesen Ansatz nicht. Ich habe an Code-Basen gearbeitet, die automatisch Testmethoden entwickelten (in einem Fall mit einer Metaklasse) und festgestellt, dass es viel schwieriger zu verstehen war, als es nützlich war. Wenn ein Test fehlschlug, war es schwierig, die Quelle des Fehlerfalls herauszufinden, und es war schwierig, den Debugging-Code beizubehalten, um nach dem Grund für den Fehler zu suchen.

(Debugging-Fehler in dem Beispiel, das ich hier geschrieben habe, ist nicht so schwer wie der spezifische Metaklassen-Ansatz, mit dem ich arbeiten musste.)

0

eine einzige Testmethode schreiben, die alle Tests und erfasst alle Ergebnisse führen, Ihre eigenen Diagnosemeldungen an stderr, schreiben und den Test nicht bestehen, wenn einer ihrer Untertests fehlschlagen:

def test_with_multiple_parameters(self): 
    failed = False 
    for k in sorted(self.test_parameters.keys()): 
     if not self.my_test(self.test_parameters[k]): 
      print >> sys.stderr, "Test {0} failed.".format(k) 
      failed = True 
    self.assertFalse(failed)    

Hinweis dass der Name my_test() natürlich nicht mit test beginnen kann.

1

Ein datenorientierten Ansatz könnte sein, klarer als die verwendeten in Andrew Dalke ‚s answer:

"""Parametrized unit test. 

Builds a single TestCase class which tests if its 
    `somevalue` method is equal to the numbers 1 through 4. 

This is accomplished by 
    creating a list (`cases`) 
    of dictionaries which contain test specifications 
    and then feeding the list to a function which creates a test case class. 

When run, the output shows that three of the four cases fail, 
    as expected: 

>>> import sys 
>>> from unittest import TextTestRunner 
>>> run_tests(TextTestRunner(stream=sys.stdout, verbosity=9)) 
... # doctest: +ELLIPSIS 
Test if self.somevalue equals 4 ... FAIL 
Test if self.somevalue equals 1 ... FAIL 
Test if self.somevalue equals 3 ... FAIL 
Test if self.somevalue equals 2 ... ok 
<BLANKLINE> 
====================================================================== 
FAIL: Test if self.somevalue equals 4 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
    ... 
AssertionError: 2 != 4 
<BLANKLINE> 
====================================================================== 
FAIL: Test if self.somevalue equals 1 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
    ... 
AssertionError: 2 != 1 
<BLANKLINE> 
====================================================================== 
FAIL: Test if self.somevalue equals 3 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
    ... 
AssertionError: 2 != 3 
<BLANKLINE> 
---------------------------------------------------------------------- 
Ran 4 tests in ...s 
<BLANKLINE> 
FAILED (failures=3) 
""" 

from unittest import TestCase, TestSuite, defaultTestLoader 

cases = [{'name': "somevalue_equals_one", 
      'doc': "Test if self.somevalue equals 1", 
      'value': 1}, 
     {'name': "somevalue_equals_two", 
      'doc': "Test if self.somevalue equals 2", 
      'value': 2}, 
     {'name': "somevalue_equals_three", 
      'doc': "Test if self.somevalue equals 3", 
      'value': 3}, 
     {'name': "somevalue_equals_four", 
      'doc': "Test if self.somevalue equals 4", 
      'value': 4}] 

class BaseTestCase(TestCase): 
    def setUp(self): 
     self.somevalue = 2 

def test_n(self, n): 
    self.assertEqual(self.somevalue, n) 

def make_parametrized_testcase(class_name, base_classes, test_method, cases): 
    def make_parametrized_test_method(name, value, doc=None): 
     def method(self): 
      return test_method(self, value) 
     method.__name__ = "test_" + name 
     method.__doc__ = doc 
     return (method.__name__, method) 

    test_methods = (make_parametrized_test_method(**case) for case in cases) 
    class_dict = dict(test_methods) 
    return type(class_name, base_classes, class_dict) 


TestCase = make_parametrized_testcase('TestOneThroughFour', 
             (BaseTestCase,), 
             test_n, 
             cases) 

def make_test_suite(): 
    load = defaultTestLoader.loadTestsFromTestCase 
    return TestSuite(load(TestCase)) 

def run_tests(runner): 
    runner.run(make_test_suite()) 

if __name__ == '__main__': 
    from unittest import TextTestRunner 
    run_tests(TextTestRunner(verbosity=9)) 

Ich bin nicht sicher, was Voodoo bei der Bestimmung die Reihenfolge beteiligt ist, in dem die Tests ausgeführt, aber der doctest geht konsequent für mich mindestens durch.

Für komplexere Situationen ist es möglich, das values Element der cases Wörterbücher durch ein Tupel zu ersetzen, das eine Liste von Argumenten und ein Dict von Schlüsselwortargumenten enthält. An diesem Punkt kodieren Sie grundsätzlich Lisp in Python.

11

Einige der verfügbaren Werkzeuge für die in Python parametrisiert Tests zu tun sind:

+0

[Parametrisierte Testfälle] (http://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases) von Eli Bendersky hat super für mich gearbeitet. –