2009-11-04 5 views
26

Possible en double:
How to generate dynamic (parametrized) unit tests in python?Rédaction d'un réutilisable (paramétrés) Méthode unittest.TestCase

J'écris des tests en utilisant le package unittest, et je veux éviter le code répété . Je vais effectuer un certain nombre de tests qui nécessitent tous une méthode très similaire, mais avec une seule valeur différente à chaque fois. Un exemple simpliste et inutile serait:

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) 

Est-il possible d'écrire l'exemple ci-dessus sans répéter tout le code à chaque fois, mais l'écriture à la place une méthode générique, par exemple

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

et dire à unittest d'essayer ce test avec des entrées différentes?

+0

Avez-vous trouvé une façon de le faire? Ou peut-être avez-vous trouvé un autre outil pour cette tâche? J'ai besoin d'un tel comportement non plus. – legesh

+0

http://thebongtraveller.blogspot.sg/2015/12/art-of-unittest-writing-auto-generation.html Est-ce la même chose? –

Répondre

0

Peut-être quelque chose comme:

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

Ce n'est pas ce que je cherche car il s'arrêtera dès qu'un des tests échouera. Je suis à la recherche d'une solution où l'échec d'un test n'empêche pas les autres d'être exécutés. – astrofrog

+0

@Morgoth: Pourquoi? Pourquoi faire plus de tests quand vous savez que vous avez un échec? –

+3

Parce qu'il n'y a rien à dire, les autres tests échoueront aussi. Il est important de savoir si tous les tests échouent, ou seulement un ou deux, car cela peut aider à diagnostiquer le problème. Il est bon de savoir dès le début combien d'échecs vous avez, vous ne voulez pas avoir à les réparer un par un jusqu'à ce qu'ils s'arrêtent. – astrofrog

1

Je suppose que ce que vous voulez est "tests paramétrés".

Je ne pense pas que unittest module prend en charge cette (malheureusement), mais si je ajoutiez cette fonction, il ressemblerait à quelque chose comme ceci:

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

Avec le module unittest existant, un simple décorateur comme celui-ci ne sera pas en mesure de « reproduire » les tests plusieurs fois, mais je pense que cela est faisable en utilisant une combinaison d'un décorateur et un métaclasse (métaclasse doit observer toutes les méthodes « de test * » et reproduire (sous différents noms générés automatiquement) les qui ont un décorateur appliqué).

3

Si vous voulez vraiment avoir plusieurs Unitttest, vous avez besoin de plusieurs méthodes. La seule façon d'obtenir cela est à travers une sorte de génération de code. Vous pouvez le faire à travers un métaclasses ou en modifiant légèrement la classe après la définition, y compris (si vous utilisez Python 2.6) par un décorateur de classe.

Voici une solution qui recherche les membres spéciaux 'multitest' et 'multitest_values' et les utilise pour construire les méthodes de test à la volée. Pas élégant, mais il fait à peu près ce que vous voulez:

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

Ceci est la sortie quand je le lance

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

Vous pouvez immédiatement voir certains des problèmes qui se produisent avec la génération de code. D'où vient "test_gt_1"? Je pourrais changer le nom pour le plus long "test_multitest_gt_1" mais quel test est 1? Mieux ici serait de commencer par _0 au lieu de _1, et peut-être dans votre cas, vous savez que les valeurs peuvent être utilisées comme un nom de fonction Python.

Je n'aime pas cette approche. J'ai travaillé sur des bases de code qui AutoGénéré méthodes d'essai (dans un cas, en utilisant un métaclasse) et a trouvé qu'il était beaucoup plus difficile de comprendre qu'il était utile. Lorsqu'un test a échoué, il était difficile de trouver la source de l'affaire de l'échec, et il était difficile de coller dans le code de débogage pour sonder la raison de l'échec. (Les échecs de débogage dans l'exemple que j'ai écrit ici n'est pas aussi difficile que l'approche spécifique de la métaclasse avec laquelle je devais travailler.)

0

Ecrivez une méthode d'essai unique qui effectue tous vos tests et capture tous les résultats, écrire vos propres messages de diagnostic à stderr, et échouent le test si l'un de ses sous-tests échouent:

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)    

Remarque bien sûr, le nom de my_test() ne peut pas commencer par test.

1

Une approche plus axée sur les données pourrait être plus clair que celui utilisé dans l » answerAndrew Dalke:

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

Je ne suis pas sûr de ce que voodoo est impliqué dans la détermination de l'ordre dans lequel les tests sont exécutés, mais le doctest passe toujours pour moi, au moins.

Pour les situations plus complexes, il est possible de remplacer l'élément values des dictionnaires cases par un tuple contenant une liste d'arguments et une dict de mots-clés. Bien qu'à ce stade, vous êtes en train de coder lisp en python.

11

Certains des outils disponibles pour faire des tests paramétrées en Python sont:

+0

[Cas de tests paramétrisés] (http://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases) d'Eli Bendersky a très bien fonctionné pour moi. –