Jak generować dynamiczne (sparametryzowane) testy jednostkowe w Pythonie?

234

Mam jakieś dane testowe i chcę utworzyć test jednostkowy dla każdego elementu. Moim pierwszym pomysłem było zrobienie tego w ten sposób:

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

Minusem tego jest to, że obsługuje wszystkie dane w jednym teście. Chciałbym wygenerować jeden test dla każdego elementu w locie. Jakieś sugestie?

Peter Hoffmann
źródło
2
Dobry link, który może dać odpowiedź: eli.thegreenplace.net/2014/04/02/…
gaborous

Odpowiedzi:

173

Nazywa się to „parametryzacją”.

Istnieje kilka narzędzi wspierających to podejście. Na przykład:

Wynikowy kod wygląda następująco:

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)

Który wygeneruje testy:

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'

Ze względów historycznych pozostawię oryginalną odpowiedź około roku 2008):

Używam czegoś takiego:

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()
Dmitrij Mukhin
źródło
24
W rzeczywistości bignose, ten kod NIE generuje innej nazwy dla każdego testu (w rzeczywistości nie działałby inaczej). W podanym przykładzie wykonane testy zostaną nazwane odpowiednio „test_foo”, „test_bar” i „test_lee”. Tak więc korzyść, o której wspominasz (i jest duża) jest zachowana, dopóki wygenerujesz rozsądne nazwy.
Toji
1
Jak podaje odpowiedź @codeape, nos sobie z tym radzi. Jednak nos nie obsługuje Unicode; dlatego dla mnie jest to preferowane rozwiązanie. +1
Keith Pinson
5
Zauważ, że bardziej odpowiednia odpowiedź znajduje się w zduplikowanym pytaniu: stackoverflow.com/a/2799009/322020 - musisz .__name__ =włączyć .exact_methodtestowanie
Nakilon
7
Dlaczego kod modyfikujący klasę pojawia się w if __name__ == '__main__'warunku? Z pewnością powinien wyjść poza to, aby działać w czasie importu (pamiętając, że moduły Pythona są importowane tylko raz, nawet jeśli są importowane z kilku różnych miejsc)
SpoonMeiser
4
Nie sądzę, że to dobre rozwiązanie. Kod unittest nie powinien zależeć od sposobu, w jaki zostanie wywołany. TestCase powinien być użyteczny w nosie, pytestach lub w innym środowisku testowym.
guettli
146

Używanie unittest (od 3.4)

Od wersji Python 3.4 standardowy unittestpakiet bibliotek ma subTestmenedżera kontekstu.

Zobacz dokumentację:

Przykład:

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)

Możesz także określić niestandardowy komunikat i wartości parametrów, aby subTest():

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

Używanie nosa

Struktura testowania nosa obsługuje to .

Przykład (poniższy kod to cała zawartość pliku zawierającego 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

Dane wyjściowe polecenia 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
źródło
3
To bardzo czysty sposób na dynamiczne generowanie przypadków testowych.
wredny
Należy jednak pamiętać, że „setup ()” nie będzie wiedział, jakie zmienne są używane jako argumenty do uzyskania. W rzeczywistości setup () nie będzie wiedział, który test jest uruchomiony, lub zmienne ustawione wewnątrz test_generator (). To komplikuje sprawdzanie poczytalności w setup () i jest jednym z powodów, dla których niektórzy ludzie wolą py.test.
Scott Prive
1
Zatwierdzono w sekcji dotyczącej aktualizacji. Dokładnie to, czego potrzebowałem. :)
Saurabh Shrivastava
1
Czy istnieje sposób na uruchomienie najbardziej nieprzystosowanej wersji z pytest, aby działały wszystkie przypadki i nie zatrzymywały się przy pierwszym nieudanym parametrze?
kakk11 24.0419
1
Jak wspomniano w @ kakk11, ta odpowiedź (i ogólnie podtest) nie działa z pytestem. To znany problem. Aktywnie opracowano wtyczkę, aby ta funkcja
Jérémie
76

Można to elegancko rozwiązać za pomocą Metaklas:

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()
Chłopak
źródło
1
To działało WIELKO dla mnie z Selenium. Uwaga: w klasie TestSequence można zdefiniować metody „statyczne”, takie jak setUp (self), is_element_present (self, how, what), ... tearDown (self). Umieszczając je po „ metaklasa = TestSequenceMeta” oświadczenie wydaje się działać.
Miłość i pokój - Joe Codeswell
5
To rozwiązanie jest lepsze niż wybrane jako zaakceptowane IMHO.
petroslamb
2
@petroslamb __new__Metoda metaklasy jest wywoływana, gdy sama klasa jest zdefiniowana, a nie podczas tworzenia pierwszej instancji. Wyobrażam sobie, że ta metoda dynamicznego tworzenia metod testowych jest bardziej kompatybilna z introspekcją używaną unittestdo określania liczby testów w klasie (tj. Może skompilować listę testów, zanim stworzy instancję tej klasy).
BillyBBone
11
Uwaga: w pythonie 3 zmień to na:class TestSequence(unittest.TestCase, metaclass=TestSequenceMeta):[...]
Mathieu_Du,
3
Czy możesz użyć dctzamiast dict? Używanie słów kluczowych jako nazw zmiennych jest mylące i podatne na błędy.
npfoss,
49

Począwszy od wersji Python 3.4, w tym celu wprowadzono unittest. Szczegółowe informacje można znaleźć w dokumentacji . TestCase.subTest to menedżer kontekstu, który pozwala izolować potwierdzenia w teście, dzięki czemu niepowodzenie zostanie zgłoszone z informacją o parametrach, ale nie zatrzyma wykonania testu. Oto przykład z dokumentacji:

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)

Wynikiem uruchomienia testowego byłoby:

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

Jest to również część unittest2 , więc jest dostępna dla wcześniejszych wersji Pythona.

Bernhard
źródło
1
Najlepsze rozwiązanie, jeśli używasz Pythona 3.4 i nowszych.
Max Malysh,
4
Przy użyciu unittest2 jest to również dostępne dla Python 2.7.
Bernhard
11
Jedną z głównych różnic między tym podejściem a oddzielnymi testami jest to, że stan testu nie jest resetowany za każdym razem. (To znaczy setUp()i tearDown()nie są przeprowadzane między podtestami.)
Kevin Christopher Henry
1
@KevinChristopherHenry Tak, ale self.setUp()teoretycznie można je wywołać ręcznie z podtestu. Jeśli chodzi o tearDown, automatyczne wywołanie go na końcu może wystarczyć.
Acumenus
Myślę, że może to być potężne, gdy zostanie użyte w połączeniu z powyższym podejściem do metaklasy.
Nathan Chappell
36

load_tests to mało znany mechanizm wprowadzony w 2.7 do dynamicznego tworzenia TestSuite. Dzięki niemu możesz łatwo tworzyć sparametryzowane testy.

Na przykład:

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

Ten kod uruchomi wszystkie TestCases w TestSuite zwrócone przez load_tests. Mechanizm wykrywania nie uruchamia automatycznie żadnych innych testów.

Możesz także użyć dziedziczenia, jak pokazano na tym bilecie: http://bugs.python.org/msg151444

Javier
źródło
1
Powyższy kod zawodzi: TypeError: __init __ () przyjmuje maksymalnie 2 argumenty (4 podane)
maks.
2
Dodano wartości domyślne null do dodatkowych parametrów konstruktora.
Javier
Wolę kod parametryzujący nos w odpowiedzi @ mojo , ale dla moich klientów zbyt przydatne jest unikanie dodatkowej zależności, więc użyję tego dla nich.
mędrzec
1
To rozwiązanie było moim ulubionym na tej stronie. Zarówno Nos , sugerowany w bieżącej górnej odpowiedzi, jak i jego widelec Nos2 służą jedynie do konserwacji, a ta ostatnia sugeruje, że użytkownicy zamiast tego wypróbują pytanie . Co za bałagan - będę trzymać się takiego rodzimego podejścia!
Sean
1
bonus: możliwość przedefiniowania metody shortDescription dla wyników przekazywanych w params
fun_vit
33

Można to zrobić za pomocą zapytania . Po prostu napisz plik test_me.pyz zawartością:

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

I uruchom test za pomocą polecenia py.test --tb=short test_me.py. Wynik będzie wyglądał następująco:

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

To proste! Również pytest ma więcej funkcji, takich jak fixtures, mark, assertitp ...

Siergiej Woroneżski
źródło
1
Szukałem prostego, prostego przykładu, jak parametryzować przypadki testowe za pomocą py.test. Dziękuję Ci bardzo!
timgeb
@timgeb Cieszę się, że mogę ci pomóc. Sprawdź tag py.test , aby uzyskać więcej przykładów. Sugeruję również użycie hamcrest do dodania cukru do twoich twierdzeń za pomocą czytelnych dla człowieka mulderów, które można modyfikować, łączyć lub tworzyć na swój własny sposób. Dodatkowo mamy wabik-python , ładnie wyglądającą generację raportów dlapy.test
Sergey Voronezhskiy
Dzięki. Właśnie zacząłem przenosić się unittestdo py.test. Kiedyś miałem TestCaseklasy podstawowe, które były w stanie dynamicznie tworzyć dzieci z różnymi argumentami, które zapisywałyby jako zmienne klasowe ... co było trochę niewygodne.
timgeb
1
@timgeb Tak masz rację. Najbardziej cechą zabójca o py.testto yield_fixtures . Co może zrobić konfigurację , zwróć kilka użytecznych danych do testu, a po zakończeniu testu usuń . Urządzenia można również parametryzować .
Sergey Voronezhskiy
12

Użyj biblioteki ddt . Dodaje proste dekoratory do metod testowych:

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

Tę bibliotekę można zainstalować za pomocą pip. Nie wymaga nosei działa doskonale ze standardowym unittestmodułem bibliotecznym .

Mychajło Kopytonienko
źródło
6

Przydałoby się wypróbowanie biblioteki TestScenarios .

Testcenarios zapewnia czyste wstrzykiwanie zależności dla testów w stylu Python Unittest. Można to wykorzystać do testowania interfejsu (testowanie wielu implementacji za pomocą jednego zestawu testów) lub do klasycznego wstrzykiwania zależności (dostarczanie testów z zależnościami zewnętrznie do samego kodu testowego, umożliwiając łatwe testowanie w różnych sytuacjach).

duży nos
źródło
4

Możesz użyć wtyczki nose-ittr ( pip install nose-ittr).

Bardzo łatwo jest zintegrować z istniejącymi testami, wymagane są minimalne zmiany (jeśli występują). Wspiera on także nos wtyczki wieloprocesorowe.

Nie to, że możesz również mieć funkcję dostosowywania setupna test.

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

Możliwe jest również przekazanie nosetestparametrów, takich jak ich attribwbudowana wtyczka , w ten sposób można uruchomić tylko określony test z określonym parametrem:

nosetest -a number=2
Maroun
źródło
Podoba mi się to podejście, zwłaszcza obsługiwany przez niego poziom metody.
Matt
3

Używam metaklas i dekoratorów do generowania testów. Możesz sprawdzić moją implementację python_wrap_cases . Ta biblioteka nie wymaga żadnych ram testowych.

Twój przykład:

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)

Dane wyjściowe konsoli:

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

Możesz także używać generatorów . Na przykład ten kod generuje wszystkie możliwe kombinacje testów z argumentami a__listib__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)

Dane wyjściowe konsoli:

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
źródło
2

Natknąłem ParamUnittest drugi dzień, kiedy patrząc na kod źródłowy do radonu ( Przykład użycia na repo github ). Powinien współpracować z innymi frameworkami rozszerzającymi TestCase (np. Nose).

Oto przykład:

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)
Matt
źródło
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)

WYNIK:

>>> 
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
źródło
1
Drobny problem z twoją def add_test_methodsfunkcją. Powinien być def _add_test_methods , myślę
Raychaser,
@Raychaser ... Masz rację ... Naprawiłem to, ale nie zaktualizowałem tutaj ... Dziękuję za złapanie tego.
Arindam Roychowdhury
1

Po prostu użyj metaklasy, jak pokazano tutaj;

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

Wynik:

test_sample (ExampleTestCase) ... OK
senny
źródło
1

Możesz używać klas TestSuiteniestandardowych TestCase.

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
źródło
Podczas działania TestSuite argumenty nie są przekazywane do __init__funkcji.
jadelord
1

Przekonałem się, że działa to dobrze dla moich celów, szczególnie jeśli muszę wygenerować testy, które nieznacznie różnicują procesy w zbiorze danych.

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

TestGeneratorKlasy mogą być wykorzystywane do tarła różne zestawy testów, takich jak TestCluster.

TestClustermożna uznać za implementację TestGeneratorinterfejsu.

bcdan
źródło
1

To rozwiązanie działa z unittesti nosedla Python 2 i 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()
wycierać
źródło
Dziękuję @ guillaume-jacquenot za zaktualizowaną wersję <3!
mop
0

Miałem problem z bardzo szczególnym stylem sparametryzowanych testów. Wszystkie nasze testy Selenium mogą być uruchamiane lokalnie, ale powinny być również możliwe do uruchomienia na kilku platformach w SauceLabs. Zasadniczo chciałem wziąć dużą liczbę już napisanych przypadków testowych i sparametryzować je przy jak najmniejszej możliwej zmianie kodu. Ponadto musiałem móc przekazać parametry do metody setUp, czegoś, czego nie widziałem w żadnym innym rozwiązaniu.

Oto, co wymyśliłem:

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

Dzięki temu wystarczyło dodać prosty dekorator @sauce_labs () do każdego standardowego starego TestCase, a teraz podczas ich uruchamiania są one pakowane i przepisywane, dzięki czemu wszystkie metody testowe są parametryzowane i zmieniane nazwy. LoginTests.test_login (self) działa jako LoginTests.test_login_internet_explorer_10.0 (self), LoginTests.test_login_internet_explorer_11.0 (self) i LoginTests.test_login_firefox_43.0 (self), a każdy z nich decyduje o tym, który parametr self / przeglądarka ma do wyboru platforma, z którą można się uruchomić, nawet w LoginTests.setUp, co jest kluczowe dla mojego zadania, ponieważ tam inicjowane jest połączenie z SauceLabs.

W każdym razie mam nadzieję, że może to pomóc komuś, kto chce przeprowadzić podobną „globalną” parametryzację swoich testów!

Danielle Weisz
źródło
0

Odpowiedzi oparte na metaklasach nadal działają w Python3, ale zamiast __metaclass__atrybutu należy użyć metaclassparametru, jak w:

class ExampleTestCase(TestCase,metaclass=DocTestMeta):
    pass
Patrick Ohly
źródło
0

Metaprogramowanie jest fajne, ale może się przydać. Większość rozwiązań utrudnia:

  • selektywnie uruchom test
  • wskaż kod podaną nazwą testu

Tak więc moją pierwszą sugestią jest pójście prostą / jawną ścieżką (działa z dowolnym testerem):

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

Ponieważ nie powinniśmy się powtarzać, moja druga sugestia opiera się na odpowiedzi @ Javiera: objąć testy oparte na właściwościach. Biblioteka hipotez:

  • jest „bardziej bezlitośnie przebiegły w generowaniu przypadków testowych niż my, zwykli ludzie”
  • dostarczy prostych przykładów
  • współpracuje z dowolnym biegaczem testowym
  • ma wiele innych interesujących funkcji (statystyki, dodatkowe wyniki testów, ...)

    klasa TestSequence (unittest.TestCase):

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

Aby przetestować konkretne przykłady, wystarczy dodać:

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

Aby uruchomić tylko jeden konkretny przykład, możesz skomentować pozostałe przykłady (pod warunkiem, że pierwszy zostanie uruchomiony). Możesz użyć @given(st.nothing()). Inną opcją jest zastąpienie całego bloku:

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

Ok, nie masz odrębnych nazw testów. Ale może potrzebujesz:

  • opisowa nazwa testowanej właściwości.
  • które wejście prowadzi do niepowodzenia (przykład fałszowania).

Zabawniejszy przykład

YvesgereY
źródło
0

Bardzo późno na przyjęcie, ale miałem problemy z wykonaniem tych prac setUpClass.

Oto wersja odpowiedzi @ Javiera, która daje setUpClassdostęp do dynamicznie przydzielanych atrybutów.

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

Wyjścia

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

OK
hhquark
źródło
0

Wystarczy wrzucić kolejne rozwiązanie do mieszanki;)

Jest to faktycznie takie samo parameterizedjak wspomniane powyżej, ale specyficzne dla 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

Przykładowe użycie:

class TestStuff(unittest.TestCase):
    @sub_test([
        dict(arg1='a', arg2='b'),
        dict(arg1='x', arg2='y'),
    ])
    def test_stuff(self, a, b):
        ...
Eric Cousineau
źródło
-1

Oprócz używania setattr, możemy używać load_tests od Pythona 3.2. Proszę odnieść się do postu na blogu 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
źródło
-1

Oto moje rozwiązanie. Uważam to za przydatne, gdy: 1. Powinien działać dla Unittest. Testuj i Unittest Discover 2. Przygotuj zestaw testów dla różnych ustawień parametrów. 3. Bardzo prosta brak zależności od innych pakietów

    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
źródło
To nie odpowiada na pytanie, które dotyczy generowania testów w locie.
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
źródło
wygląda na to, że straciłeś tam formatowanie. naprawdę trudno jest go odczytać w obecnej formie
Arturo