Comment générer des tests unitaires dynamiques (paramétrés) en python ?
-
09-06-2019 - |
Question
J'ai une sorte de données de test et je souhaite créer un test unitaire pour chaque élément.Ma première idée était de procéder ainsi :
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 gère toutes les données en un seul test.Je voudrais générer un test pour chaque élément à la volée.Aucune suggestion?
La solution
j'utilise quelque chose comme ceci :
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 parameterized
Le package 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)
Ce qui générera les tests :
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'
Autres conseils
Utilisation de unittest (depuis 3.4)
Depuis Python 3.4, la bibliothèque standard unittest
le paquet a le subTest
gestionnaire de contexte.
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ètres à subTest()
:
with self.subTest(msg="Checking if p1 equals p2", p1=p1, p2=p2):
Utiliser le nez
Le nez cadre de test soutient cela.
Exemple (le code ci-dessous est l'intégralité 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
Le résultat de la commande 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)
Cela peut être résolu avec élégance en utilisant les 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()
Depuis Python 3.4, des sous-tests ont été introduits dans unittest à cet effet.Voir La documentation pour plus de détails.TestCase.subTest est un gestionnaire de contexte qui permet d'isoler les assertions dans un test afin qu'un échec soit signalé avec les informations sur les paramètres mais n'arrête pas l'exécution du test.Voici l'exemple 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)
Le résultat d’un test 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
Cela fait également partie de test unitaire2, il est donc disponible pour les versions antérieures de Python.
chargement_tests est un mécanisme peu connu introduit dans la version 2.7 pour créer dynamiquement une TestSuite.Avec lui, 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 tous les TestCases de la TestSuite renvoyés par load_tests.Aucun autre test n'est automatiquement exécuté par le mécanisme de découverte.
Alternativement, vous pouvez également utiliser l'héritage comme indiqué dans ce ticket : http://bugs.python.org/msg151444
Cela peut être fait en utilisant test py.Écrivez simplement le fichier test_me.py
avec 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 lancez votre test avec la commande py.test --tb=short test_me.py
.Le résultat ressemblera alors à :
=========================== 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 test py a plus de fonctionnalités comme fixtures
, mark
, assert
, etc ...
Utilisez le ddt bibliothèque.Il ajoute des décorateurs simples pour les méthodes de test :
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
.Cela ne nécessite pas nose
, et fonctionne parfaitement avec la bibliothèque standard unittest
module.
Vous gagneriez à essayer le Scénarios de test bibliothèque.
testscenarios fournit une injection de dépendance propre pour les tests de style python unittest.Cela peut être utilisé pour les tests d'interface (tester de nombreuses implémentations via une seule suite de tests) 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).
Il existe également Hypothesis qui ajoute des tests fuzz ou basés sur des propriétés : https://pypi.python.org/pypi/hypothesis
Il s'agit d'une méthode de test très puissante.
Vous pouvez utiliser nez-ittr brancher (pip install nose-ittr
).
Il est très facile à intégrer aux tests existants, des modifications minimes (le cas échéant) sont nécessaires.Il prend également en charge nez plugin multitraitement.
Non pas que vous puissiez également personnaliser setup
fonction par test.
@ittr(number=[1, 2, 3, 4])
def test_even(self):
assert_equal(self.number % 2, 0)
Il est également possible de passer nosetest
paramètres comme avec leur plugin intégré attrib
, de cette façon, vous pouvez exécuter uniquement un test spécifique avec un paramètre spécifique :
nosetest -a number=2
Je suis tombé sur ParamUnittest l'autre jour, en regardant le code source pour radon (exemple d'utilisation sur le dépôt github).Il devrait fonctionner avec d'autres frameworks qui étendent TestCase (comme Nose).
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)
J'utilise des métaclasses et des décorateurs pour générer des tests.Vous pouvez vérifier ma mise en œuvre python_wrap_cases.Cette bibliothèque ne nécessite aucun framework 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 des 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
Utilisez simplement des métaclasses, comme vu 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
Sortir:
test_sample (ExampleTestCase) ... OK
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)
Vous pouvez utiliser TestSuite
et personnalisé TestCase
Des classes.
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)
J'avais des problèmes avec un style très particulier de tests paramétrés.Tous nos tests Selenium peuvent être exécutés localement, mais ils devraient également pouvoir être exécutés à distance sur plusieurs plates-formes sur SauceLabs.Fondamentalement, je voulais prendre une grande quantité de cas de test déjà écrits et les paramétrer avec le moins de modifications de code possible.De plus, je devais pouvoir transmettre les paramètres dans la méthode setUp, ce pour lequel je n'ai vu aucune solution ailleurs.
Voici 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 cela, tout ce que j'avais à faire était d'ajouter un simple décorateur @sauce_labs() à chaque ancien TestCase standard, et maintenant, lors de leur exécution, ils sont enveloppés et réécrits, de sorte que toutes les méthodes de test soient paramétrées et renommées.LoginTests.test_login(self) s'exécute sous les noms LoginTests.test_login_internet_explorer_10.0(self), LoginTests.test_login_internet_explorer_11.0(self) et LoginTests.test_login_firefox_43.0(self), et chacun a le paramètre self.platform pour décider quel navigateur/ plate-forme sur laquelle exécuter, même dans LoginTests.setUp, ce qui est crucial pour ma tâche puisque c'est là que la connexion à SauceLabs est initialisée.
Quoi qu'il en soit, j'espère que cela pourra être utile à quelqu'un qui cherche à faire un paramétrage "global" similaire de ses tests !
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()
Les réponses basées sur les métaclasses fonctionnent toujours en Python3, mais au lieu de __metaclass__
attribut, il faut utiliser le metaclass
paramètre, comme dans :
class ExampleTestCase(TestCase,metaclass=DocTestMeta):
pass
La méta-programmation est amusante, mais elle peut prendre du chemin.La plupart des solutions ici rendent difficile :
- lancer sélectivement un test
- pointer vers le code donné le nom du test
Donc, ma première suggestion est de suivre le chemin simple/explicite (fonctionne avec n'importe quel programme d'exécution 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()
Puisqu'il ne faut pas se répéter, ma deuxième suggestion s'appuie sur la réponse de @Javier :adopter des tests basés sur les propriétés.Bibliothèque d'hypothèses :
- est "plus sournois en matière de génération de cas de test que nous, simples humains"
- fournira des exemples simples
- fonctionne avec n'importe quel lanceur de test
possède de nombreuses fonctionnalités plus intéressantes (statistiques, sorties de tests supplémentaires, ...)
classe 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, ajoutez simplement :
@example("a", "a")
@example("a", "b")
@example("b", "b")
Pour exécuter un seul exemple particulier, vous pouvez commenter les autres exemples (à condition que l'exemple soit exécuté en premier).Vous voudrez peut-être 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 tests distincts.Mais peut-être avez-vous juste besoin de :
- un nom descriptif de la propriété testée.
- quelle entrée conduit à l’échec (exemple falsifiant).
Très tard à la fête, mais j'ai eu du mal à les faire fonctionner pour setUpClass
.
Voici une version de @ Réponse de Javier ça 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
Les sorties
1
2
..
3
4
..
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
En plus d'utiliser setattr, nous pouvons utiliser load_tests depuis python 3.2.Veuillez vous référer à l'article du 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()
Voici ma solution.Je trouve cela utile lorsque :1.Devrait fonctionner pour unittest.testcase et unittest Discover 2.Avoir un ensemble de tests à exécuter pour différents réglages de paramètres.3.Très simple aucune dépendance à l'égard des autres packages importent un 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