Как вы генерируете динамические (параметризованные) модульные тесты в 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()
Недостатком этого является то, что он обрабатывает все данные в одном тесте. Я хотел бы создать один тест для каждого элемента "на лету". Какие-либо предложения?
Ответы
Ответ 1
я использую что-то вроде этого:
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()
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'
Ответ 2
Использование 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):
Использование носа
Основа тестирования носа поддерживает это.
Пример (код ниже - это все содержимое файла, содержащего тест):
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)
Ответ 3
Это можно решить элегантно, используя метаклассы:
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()
Ответ 4
Начиная с Python 3.4 для этой цели были введены подтесты unittest. Смотрите документацию для деталей. TestCase.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.
Ответ 5
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
Ответ 6
Это можно сделать, используя 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
и т.д.
Ответ 7
Используйте библиотеку ddt. Он добавляет простые декораторы для методов тестирования:
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
library.
Ответ 8
Вам будет полезно попробовать библиотеку TestScenarios.
testscenarios обеспечивает чистую инъекцию зависимостей для тестов стиля стилей python. Это может быть использовано для тестирования интерфейса (тестирование многих реализаций через один набор тестов) или для классической инъекции зависимостей (обеспечить тесты с зависимостями снаружи самого тестового кода, что позволяет легко тестировать в разных ситуациях).
Ответ 9
Есть также гипотеза, которая добавляет нечеткое или основанное на свойствах тестирование: https://pypi.python.org/pypi/hypothesis
Это очень мощный метод тестирования.
Ответ 10
Вы можете использовать nose-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
Ответ 11
Я встретил ParamUnittest на следующий день, посмотрев исходный код на radon (пример использования в репозитории 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)
Ответ 12
Я использую метаклассы и декораторы для генерации тестов. Вы можете проверить мою реализацию 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
Ответ 13
Просто используйте метаклассы, как показано здесь;
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
Ответ 14
Вы можете использовать классы TestSuite
и custom 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)
Ответ 15
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 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)
Ответ 16
У меня были проблемы с особым стилем параметризованных тестов. Все наши тесты Selenium могут выполняться локально, но они также должны быть удалены удаленно на нескольких платформах на SauceLabs. В принципе, я хотел взять большое количество уже написанных тестовых примеров и параметризовать их с минимальными изменениями кода. Кроме того, мне нужно было передать параметры в метод setUp, что я не видел в других решениях.
Вот что я придумал:
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() в каждый обычный старый TestCase, а теперь, когда его запускают, они завертываются и переписываются, так что все методы тестирования параметризуются и переименовываются, LoginTests.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.platform, чтобы решить, какой браузер/платформу для запуска, даже в LoginTests.setUp, что имеет решающее значение для моей задачи, так как она инициализируется подключением к SauceLabs.
В любом случае, я надеюсь, что это может помочь кому-то, кто хочет сделать подобную "глобальную" параметризацию своих тестов!
Ответ 17
Это решение работает с 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()
Ответ 18
Ответы на основе метакласса по-прежнему работают в Python3, но вместо атрибута __metaclass__
необходимо использовать параметр metaclass
, как в:
class ExampleTestCase(TestCase,metaclass=DocTestMeta):
pass
Ответ 19
Мета-программирование - это весело, но может быть в пути. Большинство решений здесь затрудняют:
- выборочно запустить тест
- вернитесь к названию тестового имени кода
Итак, мое первое предложение состоит в том, чтобы следовать простому/явному пути (работает с любым тестовым бегуном):
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: включайте тестирование на основе свойств. Библиотека гипотез:
- "более неуклонно охарактеризован по поводу генерации тестового теста, чем мы, просто люди".
- предоставит простые примеры подсчета
- работает с любым тестовым бегуном
-
имеет гораздо больше интересных функций (статистика, дополнительный тестовый результат,...)
класс 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"))
Хорошо, у вас нет отдельных имен тестов. Но, возможно, вам просто нужно:
- описательное имя проверяемого свойства.
- который приводит к ошибке (пример фальсификации).
Пример Funnier
Ответ 20
Супер опоздал на вечеринку, но у меня возникли проблемы с работой над setUpClass
.
Здесь приведена версия ответа @Javier, которая дает доступ 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
Ответ 21
Помимо использования 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()
Ответ 22
Следующее - мое решение. Я считаю это полезным, когда:
1. Должен работать для unittest.Testcase и unittest обнаружить
2. Проведите набор тестов для разных параметров.
3. Очень простая зависимость от других пакетов import unittest
class BaseClass(unittest.TestCase):
def setUp(self):
self.param = 2
self.base = 2
def test_me(self):
self.assertGreaterEqual(5, self.param+self.base)
def test_me_too(self):
self.assertLessEqual(3, self.param+self.base)
class Child_One(BaseClass):
def setUp(self):
BaseClass.setUp(self)
self.param = 4
class Child_Two(BaseClass):
def setUp(self):
BaseClass.setUp(self)
self.param = 1