2009-11-04 42 views
26

Powiel możliwe:
How to generate dynamic (parametrized) unit tests in python?Pisanie wielokrotnego użytku (parametryzowane) Metoda unittest.TestCase

Piszę testy przy użyciu unittest pakiet, a chcę uniknąć powtarzającego się kodu . Mam zamiar przeprowadzić szereg testów, które wszystkie wymagają bardzo podobnej metody, ale za każdym razem tylko jedna wartość jest inna. Uproszczony i bezużyteczny Przykładem może być:

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) 

Czy istnieje sposób, aby napisać powyższy przykład bez powtarzania cały kod za każdym razem, ale zamiast pisać sposób ogólny, np

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

i mówienie unittest, aby wypróbować ten test z różnymi wejściami?

+0

Znaleźliście sposób to zrobić? A może znalazłeś inne narzędzie do tego zadania? Potrzebuję takiego zachowania. – legesh

+0

http://thebongtraveller.blogspot.sg/2015/12/art-of-unittestwriting-auto-generation.html Czy to to samo? –

Odpowiedz

0

Może coś takiego:

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

To nie jest to, czego szukam, ponieważ zatrzyma się, gdy tylko jeden z testów się nie powiedzie. Szukam rozwiązania, w którym jedna próba nie powstrzyma innych przed wykonaniem. – astrofrog

+0

@Morgoth: Dlaczego? Po co uruchamiać więcej testów, gdy wiesz, że masz awarię? –

+3

Ponieważ nie ma nic do powiedzenia, inne testy również zawiodą. Ważne jest, aby wiedzieć, czy wszystkie testy zakończą się niepowodzeniem, czy tylko jeden lub dwa, ponieważ może to pomóc w zdiagnozowaniu problemu. Miło wiedzieć od samego początku, ile błędów masz, nie chcesz ich naprawiać jeden po drugim, dopóki się nie zatrzymają. – astrofrog

1

Chyba to, co chcesz "parametryzowane testy".

Nie sądzę unittest moduł obsługuje tej (niestety), ale gdybym dodanie tej funkcji będzie wyglądać mniej więcej tak:

# 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) 

z istniejącym module unittest, prostego dekoratora takiego nie będzie w stanie "powtórzyć" testu wiele razy, ale myślę, że jest to możliwe przy użyciu kombinacji dekoratora i metaklasy (metaclass powinien obserwować wszystkie metody "testowe" i replikować (pod różnymi nazwami generowanymi automatycznie) z nałożonym dekoratorem).

3

Jeśli naprawdę chcesz mieć wiele unitestów, potrzebujesz wielu metod. Jedynym sposobem, aby to osiągnąć, jest generowanie kodu. Możesz to zrobić za pomocą metaclasses lub przez ulepszenie klasy po definicji, w tym (jeśli używasz Pythona 2.6) przez dekorator klas.

Oto rozwiązanie, które wyszukuje członków "multitest" i "multitest_values" i używa ich do budowania metod testowych w locie. Nie elegancki, ale robi mniej więcej to, co chcesz:

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() 

To jest wyjście od kiedy go uruchomić

% 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) 

można natychmiast zobaczyć niektóre z problemów, które występują z generowania kodu. Skąd pochodzi "test_gt_1"? Mógłbym zmienić nazwę na dłuższy "test_multitest_gt_1", ale który test to 1? Lepiej byłoby zacząć od _0 zamiast _1, i być może w twoim przypadku wiesz, że wartości mogą być używane jako nazwa funkcji Pythona.

Nie podoba mi się to podejście. Pracowałem nad podstawami kodu, które autogenerowały metody testowe (w jednym przypadku przy użyciu metaklasu) i odkryłem, że było to trudniejsze do zrozumienia, niż było użyteczne. Kiedy test się nie powiódł, trudno było ustalić źródło niepowodzenia i trudno było trzymać się debugowania kodu, aby zbadać przyczynę niepowodzenia.

(Usuwanie błędów w przykładzie, który tu napisałem, nie jest tak trudne, jak to podejście z metaklasem, z którym miałem pracować.)

0

Napisz pojedynczej metody badawczej, która wykonuje wszystkich testów i przechwytuje wszystkie wyniki, pisać własne komunikaty diagnostyczne do standardowego wyjścia błędów, a nie test, czy któryś z jego podtestach fail:

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)    

Note to oczywiście nazwa my_test() nie może zaczynać się od test.

1

Bardziej podejście zorientowane na dane mogą być jaśniejsze niż ta stosowana w 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)) 

nie jestem pewien, co voodoo jest zaangażowany w ustalaniu kolejności, w której prowadzone są badania, ale doktryna przechodzi przynajmniej dla mnie konsekwentnie.

Dla bardziej złożonych sytuacji istnieje możliwość zamiany elementu values w słownikach cases za pomocą krotki zawierającej listę argumentów i dyktando argumentów słów kluczowych. Chociaż w tym momencie zasadniczo kodujesz seplenienie w pytonie.

11

Niektóre z narzędzi dostępnych dla prowadzenia parametryzowane testy w Pythonie są:

+0

[Sparametryzowane przypadki testowe] (http://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases) autorstwa Eli Bendersky sprawdziły się znakomicie. –

Powiązane problemy