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

235

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

Peter Hoffmann
la source
2
Un bon lien qui peut apporter une réponse: eli.thegreenplace.net/2014/04/02/…
gaborous

Réponses:

173

C'est ce qu'on appelle la "paramétrisation".

Il existe plusieurs outils qui soutiennent cette approche. Par exemple:

Le code résultant ressemble à ceci:

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 va générer 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'

Pour des raisons historiques, je laisserai la réponse originale vers 2008):

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()
Dmitry Mukhin
la source
24
En fait, bignose, ce code génère un nom différent pour chaque test (cela ne fonctionnerait pas autrement). Dans l'exemple donné, les tests exécutés seront nommés "test_foo", "test_bar" et "test_lee" respectivement. Ainsi, l'avantage que vous mentionnez (et il est important) est conservé tant que vous générez des noms sensés.
Toji
1
Comme l'indique la réponse donnée par @codeape, nose gère cela. Cependant, le nez ne semble pas gérer Unicode; c'est donc pour moi une solution préférable. +1
Keith Pinson
5
Donc, notez que cette réponse plus appropriée est donnée dans la question en double : stackoverflow.com/a/2799009/322020 - vous avez utilisé .__name__ =pour activer les .exact_methodtests
Nakilon
7
Pourquoi le code modifiant la classe apparaît-il au if __name__ == '__main__'conditionnel? Il devrait sûrement aller au-delà de cela pour s'exécuter au moment de l'importation (en se souvenant que les modules python ne sont importés qu'une seule fois même s'ils sont importés de plusieurs endroits différents)
SpoonMeiser
4
Je ne pense pas que ce soit une bonne solution. Le code d'un unittest ne doit pas dépendre de la façon dont il est appelé. Le TestCase doit être utilisable dans le nez ou le pytest ou dans un environnement de test différent.
guettli
147

Utiliser unittest (depuis 3.4)

Depuis Python 3.4, le unittestpackage de bibliothèque standard a le subTestgestionnaire 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 pour subTest():

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

Utiliser le nez

Le cadre de test de nez prend en charge 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

La sortie 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)
codeape
la source
3
C'est une façon très propre de générer dynamiquement des cas de test.
2015
Mais sachez que 'setup ()' ne saura pas quelles variables sont utilisées comme arguments à produire. En fait, setup () ne saura pas quel test est en cours d'exécution, ou les variables définies dans test_generator (). Cela complique la vérification d'intégrité dans setup (), et c'est l'une des raisons pour lesquelles certaines personnes préfèrent py.test.
Scott Prive
1
A voté pour la section mise à jour. Exactement ce dont j'avais besoin. :)
Saurabh Shrivastava
1
Existe-t-il un moyen d'exécuter la version la plus complète avec pytest, afin qu'elle exécute tous les cas et ne s'arrête pas sur le premier paramètre ayant échoué?
kakk11
1
Comme mentionné par @ kakk11, cette réponse (et subTest en général) ne fonctionne pas avec pytest. C'est un problème connu. Il existe un plugin activement développé pour faire ce travail: github.com/pytest-dev/pytest-subtests
Jérémie
76

Cela peut être résolu avec élégance à l'aide de 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()
Gars
la source
1
Cela a fonctionné GRAND pour moi avec Selenium. Remarque: dans la classe TestSequence, vous pouvez définir des méthodes "statiques" comme setUp (self), is_element_present (self, how, what), ... tearDown (self). Les mettre après le « métaclasse déclaration = TestSequenceMeta » semble fonctionner.
Amour et paix - Joe Codeswell
5
Cette solution est meilleure que celle sélectionnée comme IMHO acceptée.
petroslamb
2
@petroslamb La __new__méthode dans la métaclasse est appelée lorsque la classe elle-même est définie, pas lorsque la première instance est créée. J'imagine que cette méthode de création dynamique de méthodes de test est plus compatible avec l'introspection utilisée par unittestpour déterminer le nombre de tests dans une classe (c'est-à-dire qu'elle peut compiler la liste des tests avant de créer une instance de cette classe).
BillyBBone
11
Remarque: en python 3, changez ceci en:class TestSequence(unittest.TestCase, metaclass=TestSequenceMeta):[...]
Mathieu_Du
3
Pourriez-vous s'il vous plaît utiliser à la dctplace de dict? L'utilisation de mots clés comme noms de variables est source de confusion et est source d'erreurs.
npfoss
49

Depuis Python 3.4, des sous-tests ont été introduits pour unittest à cet effet. Voir la documentation pour plus de détails. TestCase.subTest est un gestionnaire de contexte qui permet d'isoler des assertions dans un test afin qu'un échec soit signalé avec des 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)

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

Cela fait également partie de unittest2 , il est donc disponible pour les versions antérieures de Python.

Bernhard
la source
1
La meilleure solution si vous utilisez python 3.4 et supérieur.
Max Malysh
4
En utilisant unittest2, cela est également disponible pour Python 2.7.
Bernhard
11
Une différence majeure entre cette approche et les tests séparés est que l'état de test n'est pas réinitialisé à chaque fois. (C'est-à-dire, setUp()et tearDown()ne sont pas exécutés entre les sous-tests.)
Kevin Christopher Henry
1
@KevinChristopherHenry Oui, mais self.setUp()peut en théorie être appelé manuellement depuis le sous-test. Quant à tearDown, le faire appeler automatiquement à la fin pourrait suffire.
Acumenus
Je pense que cela pourrait être puissant lorsqu'il est utilisé en conjonction avec l'approche métaclasse ci-dessus.
Nathan Chappell
36

load_tests est un mécanisme peu connu introduit en 2.7 pour créer dynamiquement un 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 dans la TestSuite retournée par load_tests. Aucun autre test n'est exécuté automatiquement 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

Javier
la source
1
Le code ci-dessus échoue: TypeError: __init __ () prend au plus 2 arguments (4 donnés)
max
2
Ajout de valeurs par défaut nulles aux paramètres supplémentaires du constructeur.
Javier
Je préfère le code de paramétrage du nez dans la réponse de @ mojo , mais pour mes clients, il est tout simplement trop utile pour éviter une dépendance supplémentaire, donc je vais l'utiliser pour eux.
sage
1
Cette solution était ma préférée sur cette page. Les deux nez , suggéré dans la réponse actuelle supérieure et sa fourchette Nose2 sont l' entretien que, et celui - ci suggère les utilisateurs tentent plutôt pytest . Quel gâchis - je m'en tiendrai à une approche native comme celle-ci!
Sean
1
bonus: possibilité de redéfinir la méthode shortDescription pour la sortie passée dans les paramètres
fun_vit
33

Cela peut être fait en utilisant pytest . Écrivez simplement le fichier test_me.pyavec 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 lancez votre test avec la commande py.test --tb=short test_me.py. La sortie 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 pytest a plus de fonctionnalités comme fixtures, mark, assert, etc ...

Sergey Voronezhskiy
la source
1
Je cherchais un exemple simple et direct de paramétrage des cas de test avec py.test. Merci beaucoup!
timgeb
@timgeb Je suis heureux de vous aider. Vérifiez la balise py.test pour plus d'exemples. Je suggère également d'utiliser hamcrest pour ajouter du sucre dans vos assertions avec des mutchers lisibles par l'homme, qui peuvent être modifiés, combinés ou créés à votre guise. De plus, nous avons allure-python , une belle génération de rapports pourpy.test
Sergey Voronezhskiy
Merci. Je viens de commencer à passer de unittestpy.test. J'avais l'habitude d'avoir des TestCaseclasses de base qui pouvaient créer dynamiquement des enfants avec différents arguments qu'ils stockaient comme variables de classe ... ce qui était un peu lourd.
timgeb
1
@timgeb Oui, vous avez raison. Le plus caractéristique de tueur de py.testest yield_fixtures . Ce qui peut faire la configuration , renvoyer des données utiles dans le test et après la fin du test, faire le démontage . Les luminaires peuvent également être paramétrés .
Sergey Voronezhskiy
12

Utilisez la bibliothèque ddt . 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. Il ne nécessite pas noseet fonctionne parfaitement avec le unittestmodule de bibliothèque standard .

Mykhaylo Kopytonenko
la source
6

Vous auriez avantage à essayer la bibliothèque TestScenarios .

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, ce qui permet des tests faciles dans différentes situations).

gros nez
la source
4

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

Il est très facile à intégrer aux tests existants, des modifications minimales (le cas échéant) sont requises. Il prend également en charge le plugin de traitement multi- nez .

Non pas que vous puissiez également avoir une setupfonction de personnalisation par test.

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

Il est également possible de passer des nosetestparamètres comme avec leur plug-in intégré attrib, de cette façon, vous ne pouvez exécuter qu'un test spécifique avec un paramètre spécifique:

nosetest -a number=2
Maroun
la source
J'aime cette approche, en particulier le niveau par méthode qu'elle prend en charge.
Matt
3

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 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 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 des générateurs . Par exemple, ce code génère toutes les combinaisons possibles de tests avec des arguments a__listetb__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 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
Kirill Ermolov
la source
2

Je suis tombé sur ParamUnittest l'autre jour en regardant le code source du 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)
Mat
la source
2
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)
Arindam Roychowdhury
la source
1
Problème mineur avec votre def add_test_methodsfonction. Devrais- def _add_test_methods je être
Raychaser
@Raychaser ... Vous avez raison .. J'ai corrigé cela mais je ne l'ai pas mis à jour ici .... Merci d'avoir attrapé cela.
Arindam Roychowdhury
1

Utilisez simplement 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

Production:

test_sample (ExampleTestCase) ... OK
sleepycal
la source
1

Vous pouvez utiliser TestSuitedes TestCaseclasses personnalisées .

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)
Max Malysh
la source
Pendant que TestSuite fonctionne, les arguments ne sont pas transmis pour __init__fonctionner.
jadelord
1

J'ai trouvé que cela fonctionne bien pour mes besoins, surtout si j'ai besoin de générer des tests qui font des processus légèrement différents sur une collection de données.

import unittest

def rename(newName):
    def renamingFunc(func):
        func.__name__ == newName
        return func
    return renamingFunc

class TestGenerator(unittest.TestCase):

    TEST_DATA = {}

    @classmethod
    def generateTests(cls):
        for dataName, dataValue in TestGenerator.TEST_DATA:
            for func in cls.getTests(dataName, dataValue):
                setattr(cls, "test_{:s}_{:s}".format(func.__name__, dataName), func)

    @classmethod
    def getTests(cls):
        raise(NotImplementedError("This must be implemented"))

class TestCluster(TestGenerator):

    TEST_CASES = []

    @staticmethod
    def getTests(dataName, dataValue):

        def makeTest(case):

            @rename("{:s}".format(case["name"]))
            def test(self):
                # Do things with self, case, data
                pass

            return test

        return [makeTest(c) for c in TestCluster.TEST_CASES]

TestCluster.generateTests()

La TestGeneratorclasse peut être utilisée pour générer différents ensembles de cas de test comme TestCluster.

TestClusterpeut être considéré comme une implémentation de l' TestGeneratorinterface.

bcdan
la source
1

Cette solution fonctionne avec unittestet nosepour Python 2 et Python 3:

#!/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()
balai
la source
Merci @ guillaume-jacquenot pour la version améliorée <3!
vadrouille
0

J'avais eu des problèmes avec un style très particulier de tests paramétrés. Tous nos tests Selenium peuvent s'exécuter 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 changements de code possible. De plus, j'avais besoin de pouvoir passer les paramètres dans la méthode setUp, chose pour laquelle 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 ordinaire, et maintenant lors de leur exécution, ils sont emballés et réécrits, de sorte que toutes les méthodes de test sont paramétrées et renommées. LoginTests.test_login (self) s'exécute en tant que 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 / contre laquelle fonctionner, 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 pourrait être utile à quelqu'un qui cherche à faire un paramétrage "global" similaire de leurs tests!

Danielle Weisz
la source
0

Les réponses basées sur les métaclasses fonctionnent toujours en Python3, mais au lieu de l' __metaclass__attribut, il faut utiliser le metaclassparamètre, comme dans:

class ExampleTestCase(TestCase,metaclass=DocTestMeta):
    pass
Patrick Ohly
la source
0

La méta-programmation est amusante, mais peut se mettre en route. La plupart des solutions ici rendent difficile:

  • lancer sélectivement un test
  • pointer vers le code donné au nom du test

Donc, ma première suggestion est de suivre le chemin simple / explicite (fonctionne avec n'importe quel lanceur 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()

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

  • est "plus implacable sur la génération de cas de test que nous, simples humains"
  • fournira des comptages simples
  • fonctionne avec n'importe quel lanceur de test
  • a beaucoup plus de fonctionnalités intéressantes (statistiques, sortie de test supplémentaire, ...)

    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 (l'exemple fourni sera 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 test distincts. Mais peut-être avez-vous juste besoin de:

  • un nom descriptif de la propriété testée.
  • quelle entrée mène à l'échec (exemple falsifiant).

Exemple plus drôle

YvesgereY
la source
0

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

Voici une version de la réponse de @ Javier qui donne setUpClassaccè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
hhquark
la source
0

Juste pour jeter une autre solution dans le mix;)

C'est en fait le même que parameterizedcelui mentionné ci-dessus, mais spécifique à unittest:

def sub_test(param_list):
    """Decorates a test case to run it as a set of subtests."""

    def decorator(f):

        @functools.wraps(f)
        def wrapped(self):
            for param in param_list:
                with self.subTest(**param):
                    f(self, **param)

        return wrapped

    return decorator

Exemple d'utilisation:

class TestStuff(unittest.TestCase):
    @sub_test([
        dict(arg1='a', arg2='b'),
        dict(arg1='x', arg2='y'),
    ])
    def test_stuff(self, a, b):
        ...
Eric Cousineau
la source
-1

Outre l'utilisation de setattr, nous pouvons utiliser load_tests depuis python 3.2. Veuillez vous 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()
pptime
la source
-1

Voici ma solution. Je trouve cela utile lorsque: 1. Devrait fonctionner pour unittest.Testcase et unittest découvrir 2. Avoir un ensemble de tests à exécuter pour différents réglages de paramètres. 3. Très simple pas de dépendance sur d'autres paquets

    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
S.Arora
la source
Cela ne répond pas à la question, qui concerne la génération de tests à la volée.
lenz
-1
import unittest

def generator(test_class, a, b,c,d,name):
    def test(self):
        print('Testexecution=',name)
        print('a=',a)
        print('b=',b)
        print('c=',c)
        print('d=',d)

    return test

def add_test_methods(test_class):
    test_list = [[3,3,5,6, 'one'], [5,5,8,9, 'two'], [0,0,5,6, 'three'],[0,0,2,3,'Four']]
    for case in test_list:
        print('case=',case[0], case[1],case[2],case[3],case[4])
        test = generator(test_class, case[0], case[1],case[2],case[3],case[4])
        setattr(test_class, "test_%s" % case[4], test)


class TestAuto(unittest.TestCase):
    def setUp(self):
        print ('Setup')
        pass

    def tearDown(self):
        print ('TearDown')
        pass

add_test_methods(TestAuto)

if __name__ == '__main__':
    unittest.main(verbosity=1)
thangaraj1980
la source
il semble que vous ayez perdu le formatage là-bas. c'est vraiment difficile à lire en l'état
Arturo