Как вы генерируете динамические (параметризованные) модульные тесты в python?
У меня есть какие-то тестовые данные и я хочу создать модульный тест для каждого элемента. Моя первая идея была сделать это так:
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()
недостатком этого является то, что он обрабатывает все данные в одном тесте. Я хотел бы создать один тест для каждого элемента на лету. Есть предложения?
22 ответа:
Я использую что-то вроде этого:
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()
The
parameterized
пакет может быть использован для автоматизации этого процесса: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)
который будет генерировать тесты:
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'
используя unittest (начиная с 3.4)
начиная с Python 3.4, стандартная библиотека
unittest
пакета сsubTest
контекст менеджер.смотрите документацию:
пример:
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)
вы также можете указать пользовательское сообщение и значения параметров для
subTest()
:with self.subTest(msg="Checking if p1 equals p2", p1=p1, p2=p2):
через нос
The нос основы тестирования поддерживает.
пример (код ниже-это все содержимое файла, содержащего тест):
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
вывод команды 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)
Это можно решить элегантно, используя метаклассы:
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()
С этой целью в unittest были введены подтесты Python 3.4. Смотрите документация для сведения. Тестовый случай.subTest-это контекстный менеджер, который позволяет изолировать утверждения в тесте, чтобы сообщить об ошибке с информацией о параметрах, но не останавливает выполнение теста. Вот пример из документации:
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)
выход тестового запуска будет:
====================================================================== 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
это тоже часть unittest2, таким образом, он доступен для более ранних версий Python.
load_tests Это малоизвестный механизм, введенный в 2.7 для динамического создания TestSuite. С его помощью вы можете легко создавать параметризованные тесты.
например:
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
этот код будет запускать все тестовые случаи в TestSuite, возвращенные load_tests. Никакие другие тесты автоматически не запускаются механизмом обнаружения.
кроме того, вы также можете использовать наследование, как показано в этом билете: http://bugs.python.org/msg151444
это можно сделать с помощью pytest. Просто напишите файл
test_me.py
с содержанием: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
и запустить тест с помощью команды
py.test --tb=short test_me.py
. Тогда выход будет выглядеть так:=========================== 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 ====================
это просто!. Также pytest имеет больше функций, таких как
fixtures
,mark
,assert
и т. д...
использовать ДДТ библиотека. Он добавляет простые декораторы для методов тестирования:
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))
эта библиотека может быть установлен с
pip
. Это не требуетnose
, и отлично работает со стандартной библиотекойunittest
модуль.
вы выиграете от попытки TestScenarios библиотека.
testscenarios обеспечивает чистую инъекцию зависимостей для тестов стиля python unittest. Это может быть использовано для тестирования интерфейса (тестирование многих реализаций с помощью одного набора тестов) или для классической инъекции зависимостей (предоставление тестов с зависимостями извне для самого тестового кода, что позволяет легко тестировать в разных ситуациях).
можно использовать нос-ittr плагин (
pip install nose-ittr
).это очень легко интегрировать с существующими испытания, минимальные изменения (если таковые имеются) не требуется. Он также поддерживает нос многопроцессорных модуль.
не то, что вы также можете настраивать
setup
функция в тест.@ittr(number=[1, 2, 3, 4]) def test_even(self): assert_equal(self.number % 2, 0)
также можно пройти
nosetest
параметры, как с их встроенным плагиномattrib
, таким образом, вы можете запустить только определенный тест с конкретными параметр:nosetest -a number=2
есть также гипотеза, которая добавляет fuzz или тестирование на основе свойств:https://pypi.python.org/pypi/hypothesis
Это очень мощный метод тестирования.
я наткнулся на ParamUnittest на днях при просмотре исходного кода в радоновые (пример использования в репозитории github). Он должен работать с другими фреймворками, которые расширяют TestCase (например, нос).
вот пример:
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)
Я использую метаклассы и декораторы для генерации тестов. Вы можете проверить мою реализацию python_wrap_cases. Эта библиотека не требует каких-либо тестовых фреймворков.
ваш пример:
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)
консоль вывода:
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
также вы можете использовать генераторы. Например этот код генерирует все возможные комбинации тестов с аргументами
a__list
и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)
консоль вывода:
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
просто используйте метаклассы, как видно здесь;
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
выход:
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)
результат:
>>> 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)
можно использовать
TestSuite
и customTestCase
классы.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)
У меня были проблемы с очень специфическим стилем параметризованных тестов. Все наши тесты селена могут выполняться локально, но они также должны быть в состоянии работать удаленно против нескольких платформ на SauceLabs. В принципе, я хотел взять большое количество уже написанных тестовых случаев и параметризовать их с наименьшим количеством изменений в коде. Кроме того, мне нужно было передать параметры в метод настройки, что-то, для чего я не видел никаких решений в другом месте.
вот что я придумал:
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
при этом все, что мне нужно было сделать, это добавить простой декоратор @sauce_labs() к каждому обычному старому тестовому кейсу, и теперь при их запуске они завернуты и переписаны, так что все методы тестирования параметризованы и переименованы. Логинтесты.test_login (self) выполняется как LoginTests.test_login_internet_explorer_10.0(self), LoginTests.test_login_internet_explorer_11.0(self) и LoginTests.test_login_firefox_43. 0 (self), и каждый из них имеет параметр self.платформа, чтобы решить, с каким браузером / платформой работать, даже в LoginTests.настройка, которая имеет решающее значение для моей задачи, так как именно там инициализируется соединение с SauceLabs.
в любом случае, я надеюсь, что это может помочь кому-то хотите сделать подобную "глобальную" параметризация тестов!
Это решение работает с
unittest
и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()
ответы на основе метакласса по-прежнему работают в Python3, но вместо
__metaclass__
атрибут нужно использовать
мета-программирование-это весело, но может сделать по пути. Большинство решений здесь затрудняют:
- выборочно запустить тест
- укажите назад к коду имя данного теста
Итак, мое первое предложение-следовать простому / явному пути (работает с любым тестовым бегуном):
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()
поскольку мы не должны повторяться, мое второе предложение основывается на ответе @Javier: embrace property based testing. Гипотеза библиотека:
- является "более неумолимо коварным в отношении поколения тестовых случаев, чем мы, простые люди"
- обеспечит простой подсчет-примеры
- работает с любыми тестов
имеет еще много интересных функций (статистика, дополнительный тестовый выход, ...)
класс TestSequence (unittest.TestCase):
@given(st.text(), st.text()) def test_complex_property(self, a, b): self.assertEqual(a,b)
чтобы проверить ваши конкретные примеры, просто добавьте:
@example("a", "a") @example("a", "b") @example("b", "b")
для запуска только одного конкретный пример, вы можете закомментировать другие примеры (при условии, что пример будет запущен первым). Вы можете использовать
@given(st.nothing())
. Другой вариант-заменить весь блок:@given(st.just("a"), st.just("b"))
хорошо, у вас нет отдельных имен тестов. Но, может быть, вам просто нужно:
- описательное имя тестируемого свойства.
- какой вход приводит к сбою (пример фальсификации).
супер поздно на вечеринку, но мне было трудно заставить их работать для
setUpClass
.вот версия @Хавьера дает
setUpClass
доступ к динамически выделенным атрибутам.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
выходы
1 2 .. 3 4 .. ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK
кроме того, используя setattr, мы можем использовать load_tests начиная с версии Python 3.2. Пожалуйста, обратитесь к сообщению в блоге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. Должен работать на unittest.Testcase и unittest discover 2. Есть набор тестов для настройки различных параметров. 3. Очень просто отсутствие зависимости от других пакетов 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