Comment générer des tests unitaires dynamiques (paramétrés) en python?

j'ai une sorte de données de test et je veux créer un test unitaire pour chaque élément. Ma première idée a été de le faire comme ceci:

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

l'inconvénient est qu'il traite toutes les données en un seul essai. Je voudrais générer un test pour chaque élément à la volée. Toutes les suggestions?

168
demandé sur Community 2008-08-28 21:49:02

22 réponses

j'utilise quelque chose comme ça:

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

le paquet parameterized peut être utilisé pour automatiser ce processus:

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)

qui générera les essais:

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'
122
répondu Dmitry Mukhin 2018-08-29 00:36:54

utilisant unittest (depuis 3.4)

depuis Python 3.4, la bibliothèque standard "paquet 151940920" possède le gestionnaire de contexte subTest .

voir la documentation:

exemple:

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)

vous pouvez également spécifier un message personnalisé et des valeurs de paramètre à subTest() :

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

avec nez

Le nez framework de test prend en charge cette .

exemple (le code ci-dessous est la totalité du contenu du fichier contenant le test):

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

la sortie des nosetests commande:

> 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)
84
répondu codeape 2018-06-25 12:23:33

cela peut être résolu élégamment en utilisant des métaclasses:

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()
58
répondu Guy 2016-03-25 19:11:59

depuis Python 3.4 les sous-tests ont été introduits à unittest à cette fin. Voir la documentation pour plus de détails. Cas de test.subTest est un gestionnaire de contexte qui permet d'isoler les asserts dans un test afin qu'un échec soit rapporté avec des informations paramétriques mais n'arrête pas l'exécution du test. Voici l'exemple tiré de la documentation:

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)

la sortie d'un essai serait:

======================================================================
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

This fait également partie de unittest2 , il est donc disponible pour les versions précédentes de Python.

40
répondu Bernhard 2016-06-28 04:48:22

load_tests est un mécanisme peu connu introduit en 2.7 pour créer dynamiquement un test suite. Avec elle, vous pouvez facilement créer des tests paramétrés.

par exemple:

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

ce code exécutera toutes les bases de test de la suite de test retournées par load_tests. Aucun autre test n'est effectué automatiquement par le mécanisme de découverte.

alternativement, vous pouvez également utiliser l'héritage comme indiqué dans ce billet: http://bugs.python.org/msg151444

30
répondu Javier 2015-11-24 01:55:47

Il peut être fait à l'aide de pytest . Il suffit d'écrire le fichier test_me.py avec le contenu:

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

et exécutez votre test avec la commande py.test --tb=short test_me.py . Alors la sortie sera ressemble:

=========================== 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 ====================

c'est simple!. Aussi pytest a plus de fonctionnalités comme fixtures , mark , assert , etc...

26
répondu Sergey Voronezhskiy 2014-09-02 15:08:01

utilisez la bibliothèque ddt . Il ajoute des décorateurs simples pour les méthodes d'essai:

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

cette bibliothèque peut être installée avec pip . Il ne nécessite pas nose , et fonctionne excellent avec la bibliothèque standard unittest module.

8
répondu Mykhaylo Kopytonenko 2016-10-21 05:13:11

vous auriez avantage à essayer la bibliothèque TestScenarios .

testscenarios fournit une injection de dépendances propre pour les tests de style unittest de python. Cela peut être utilisé pour tester l'interface (tester de nombreuses implémentations via une seule suite de test) ou pour l'injection de dépendances classique (fournir des tests avec des dépendances externes au code de test lui-même, permettant des tests faciles dans différentes situations).

6
répondu bignose 2009-05-01 03:59:06

Vous pouvez utiliser nez-ittr plugin ( pip install nose-ittr ).

c'est très facile à intégrer avec les tests existants, des changements minimes (s'il y en a) sont nécessaires. Il supporte également nose plugin multiprocessing.

non pas que vous puissiez également avoir une fonction setup personnalisée par test.

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

il est également possible de passer les paramètres nosetest comme avec leur build-in plugin attrib , de cette façon vous pouvez exécuter seulement un test spécifique avec un paramètre spécifique:

nosetest -a number=2
4
répondu Maroun 2014-12-02 13:39:03

il y a aussi une hypothèse qui ajoute des tests fuzz ou basés sur la propriété: https://pypi.python.org/pypi/hypothesis

c'est une méthode d'essai très puissante.

4
répondu Javier 2015-11-24 02:00:00

je suis tombé sur ParamUnittest l'autre jour en regardant le code source de radon ( exemple d'usage sur le GitHub repo ). Il devrait fonctionner avec d'autres cadres qui étendent TestCase (comme nez).

voici un exemple:

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
répondu Matt 2015-03-06 01:21:50

j'utilise des métaclasses et des décorateurs pour générer des tests. Vous pouvez vérifier mon implémentation python_wrap_cases . Cette bibliothèque n'a pas besoin de cadres de test.

votre exemple:

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)

sortie de la Console:

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

vous pouvez Également utiliser générateurs . Par exemple, ce code génère toutes les combinaisons possibles de tests avec les arguments a__list et b__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)

sortie de la Console:

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
répondu Kirill Ermolov 2015-08-21 10:13:54

il suffit d'utiliser des métaclasses, comme on le voit ici;

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

sortie:

test_sample (ExampleTestCase) ... OK
1
répondu sleepycal 2015-05-10 11:43:23
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)

résultat:

>>> 
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
répondu Arindam Roychowdhury 2016-12-07 06:10:16

vous pouvez utiliser les classes TestSuite et" custom 151920920".

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)
1
répondu Max Malysh 2017-03-08 18:32:19

j'avais eu des problèmes avec un style très particulier de tests paramétrés. Tous nos tests de sélénium peuvent être exécutés localement, mais ils devraient aussi pouvoir être exécutés à distance contre plusieurs plateformes sur les SauceLabs. Fondamentalement, je voulais prendre un grand nombre de cas de test déjà écrits et les paramétrer avec le moins de changements possibles au code. De plus, je devais être capable de passer les paramètres dans la méthode de configuration, quelque chose que je n'ai pas vu de solutions pour ailleurs.

voilà ce que j'ai trouvé:

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

avec ceci, tout ce que j'avais à faire était d'ajouter un décorateur simple @sauce_labs() à chaque Vieux TestCase régulier, et maintenant quand ils les exécutent, ils sont enveloppés et réécrits, de sorte que toutes les méthodes d'essai soient paramétrées et renommées. LoginTests.test_login (self) fonctionne comme LoginTests.test_login_internet_explorer_10.0 (self), LoginTests.test_login_internet_explorer_11.0 (soi), et LoginTests.test_login_firefox_43.0 (self), et chacun a le soi paramètre.plate-forme pour décider du navigateur/plate-forme à utiliser, même dans les LoginTests.setUp, qui est crucial pour ma tâche puisque c'est là que la connexion aux SauceLabs est initialisée.

quoi qu'il en soit, j'espère que cela pourrait être utile à quelqu'un qui cherche à faire une paramétrisation "globale" similaire de leurs tests!

0
répondu Danielle Weisz 2016-04-22 08:02:46

cette solution fonctionne avec unittest et nose :

#!/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()
0
répondu mop 2016-09-06 13:57:36

les réponses basées sur metaclass fonctionnent toujours en Python3, mais au lieu de l'attribut __metaclass__ il faut utiliser le paramètre metaclass , comme dans:

class ExampleTestCase(TestCase,metaclass=DocTestMeta):
    pass
0
répondu Patrick Ohly 2017-05-08 15:25:51

de la Méta-programmation, c'est amusant, mais vous pouvez obtenir sur le chemin. La plupart des solutions ici rendent difficile de:

  • lancement sélectif d'un essai
  • pointer vers le nom du code d'essai

ainsi, ma première suggestion est de suivre le chemin simple / explicite (fonctionne avec n'importe quel coureur de test):

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

puisque nous ne devrions pas nous répéter, ma deuxième suggestion s'appuie sur la réponse de @Javier: adopter les tests basés sur la propriété. Bibliothèque d'hypothèses:

  • est "plus implacablement sournois au sujet de la génération de cas d'essai que nous simples humains"
  • fournira simple décompte des exemples
  • fonctionne avec n'importe quel coureur d'essai
  • a beaucoup plus de caractéristiques intéressantes (statistiques, résultats d'essai supplémentaires, ...)

    class TestSequence (unittest.TestCase):

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

pour tester vos exemples spécifiques, il suffit d'ajouter:

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

pour exécuter un seul exemple particulier, vous pouvez commenter les autres exemples (l'exemple fourni sera exécuté en premier). Vous pouvez utiliser @given(st.nothing()) . Une autre option consiste à remplacer le bloc entier par:

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

Ok, vous n'avez pas de noms de test distincts. Mais peut-être que vous avez juste besoin de:

  • un nom descriptif de l' propriété en cours de test.
  • qui conduit à l'échec (exemple falsificateur).

plus Drôle exemple

0
répondu YvesgereY 2017-08-08 13:59:37

Super tard à la fête, mais j'ai eu du mal à faire fonctionner ces derniers pour setUpClass .

Voici une version de la réponse de @Javier qui donne setUpClass accès aux attributs alloués dynamiquement.

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

sorties

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

OK
0
répondu hhquark 2018-01-25 21:24:22

en plus d'utiliser setattr, nous pouvons utiliser load_tests depuis python 3.2. S'il vous plaît se référer à l'article de blog blog.livreuro.com/en/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
répondu pptime 2016-05-26 22:29:51

voici ma solution. Je trouve cela utile lorsque: 1. Ça devrait marcher pour unittest.Testcase et unittest discover 2. Avoir un ensemble de tests à exécuter pour différents paramètres. 3. Très simple pas de dépendance sur d'autres paquets import 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
-1
répondu S.Arora 2016-08-02 16:35:17