Как вы генерируете динамические (параметризованные) модульные тесты в Python? - PullRequest
189 голосов
/ 28 августа 2008

У меня есть какие-то тестовые данные и я хочу создать модульный тест для каждого элемента Моей первой идеей было сделать это так:

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 ]

136 голосов
/ 28 августа 2008

Я использую что-то вроде этого:

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'
102 голосов
/ 29 августа 2008

Использование 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):

Используя нос

Фреймворк для тестирования nose поддерживает это .

Пример (код ниже содержит все содержимое файла, содержащего тест):

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 -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)
63 голосов
/ 01 января 2014

Это можно решить элегантно, используя метаклассы:

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()
42 голосов
/ 01 апреля 2015

Начиная с 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.

32 голосов
/ 07 мая 2014

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

Этот код будет запускать все TestCases в TestSuite, возвращаемые load_tests. Никакие другие тесты не запускаются автоматически механизмом обнаружения.

Кроме того, вы также можете использовать наследование, как показано в этом билете: http://bugs.python.org/msg151444

27 голосов
/ 02 сентября 2014

Это можно сделать с помощью 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 и т. Д. *

9 голосов
/ 21 апреля 2015

Используйте библиотеку 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.

6 голосов
/ 01 мая 2009

Вы бы выиграли от использования библиотеки TestScenarios .

testscenarios предоставляет чистую инъекцию зависимостей для тестов в стиле юнит-тестов python. Это может использоваться для тестирования интерфейса (тестирование многих реализаций с помощью одного набора тестов) или для классического внедрения зависимостей (предоставление тестов с зависимостями извне к самому тестовому коду, что позволяет легко тестировать в различных ситуациях).

5 голосов
/ 24 ноября 2015

Существует также гипотеза, которая добавляет нечеткое или основанное на свойствах тестирование: https://pypi.python.org/pypi/hypothesis

Это очень мощный метод тестирования.

4 голосов
/ 02 декабря 2014

Вы можете использовать плагин nose-ittr (pip install nose-ittr).

Очень легко интегрировать с существующими тестами, требуются минимальные изменения (если таковые имеются). Он также поддерживает плагин многопроцессорной обработки nose .

Не то чтобы вы могли также настроить setup функцию для каждого теста.

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

Также возможно передать nosetest параметры, как с их встроенным плагином attrib, таким образом, вы можете запустить только определенный тест с определенным параметром:

nosetest -a number=2
...