Как вы создаете динамические (параметризованные) модульные тесты в 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()

Обратной стороной является то, что он обрабатывает все данные в одном тесте. Я хотел бы на лету сгенерировать по одному тесту для каждого элемента. Какие-либо предложения?


person Peter Hoffmann    schedule 28.08.2008    source источник
comment
возможный дубликат Python unittest: генерировать несколько тестов программно?   -  person Nakilon    schedule 14.04.2013
comment
Хорошая ссылка, которая может дать ответ: eli.thegreenplace .net / 2014/04/02 /   -  person gaborous    schedule 29.07.2015


Ответы (25)


Это называется параметризацией.

Есть несколько инструментов, поддерживающих этот подход. Например.:

Полученный код выглядит так:

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'

По историческим причинам я оставлю исходный ответ примерно 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()
person Dmitry Mukhin    schedule 28.08.2008
comment
На самом деле, большой, этот код ДЕЙСТВИТЕЛЬНО генерирует разные имена для каждого теста (иначе он бы не работал). В приведенном примере выполняемые тесты будут называться test_foo, test_bar и test_lee соответственно. Таким образом, упомянутое вами преимущество (и оно очень большое) сохраняется до тех пор, пока вы генерируете разумные имена. - person Toji; 23.02.2011
comment
Как говорится в ответе @codeape, с этим справляется нос. Однако, кажется, что Нос не поддерживает Юникод; поэтому для меня это предпочтительное решение. +1 - person Keith Pinson; 14.08.2011
comment
Как говорится в ответе @codeape ниже, у носа есть поддержка для этого. Однако похоже, что он не очень хорошо поддерживает Unicode. Поэтому этот ответ предпочтительнее для тех, кто запускает тесты с данными Unicode. +1 - person Keith Pinson; 14.08.2011
comment
Есть проблема, что когда вы не можете сделать python myfile.py TestSequense.test_foo - вы получите ValueError: no such test method in <class '__main__.TestSequense'>: test_generator, поэтому вы можете делать только python myfile.py TestSequense - person Nakilon; 12.04.2013
comment
Обратите внимание, что более правильный ответ дается в вопросе duplicate: stackoverflow.com/a/2799009/322020 - вы должны использовать .__name__ = для включения .exact_method тестирования - person Nakilon; 12.04.2013
comment
Почему код, изменяющий класс, появляется в if __name__ == '__main__' условном выражении? Конечно, он должен выходить за рамки этого, чтобы запускаться во время импорта (помня, что модули python импортируются только один раз, даже если они импортированы из нескольких разных мест) - person SpoonMeiser; 21.12.2013
comment
@SpoonMeiser, это был всего лишь образец, конечно, есть масса способов улучшить ситуацию. спасибо за внимание - person Dmitry Mukhin; 21.12.2013
comment
Не думаю, что это хорошее решение. Код unittest не должен зависеть от способа его вызова. TestCase следует использовать в программе Нос, pytest или другой тестовой среде. - person guettli; 29.04.2014
comment
@mojo - Как запустить всего один тест, если вы используете нос для параметризованных тестов? Я использую pycharm IDE. Когда я щелкаю правой кнопкой мыши и запускаю параметризованный тест носа, я получаю сообщение об ошибке Error Traceback (most recent call last): File "C:\Python3\lib\unittest\case.py", line 384, in _executeTestPart function() TypeError: 'NoneType' object is not callable. Итак, мне нужно запустить все тесты, чтобы проверить один тест. Как это исправить ? - person Erran Morad; 10.10.2016
comment
@BoratSagdiyev, я ответил на этот вопрос 8 лет назад :) Боюсь, что я уже не специалист в этом вопросе. Кроме того, бит nose_parameterized был добавлен кем-то другим. Извините! - person Dmitry Mukhin; 10.10.2016
comment
У меня сложилось впечатление, что 10 лет назад это был отличный ответ. Но за него так много проголосовали, потому что это самый старый, а не лучший ответ. В настоящее время, вероятно, лучше всего использовать параметризованный декоратор py.test. (Метаклассы уродливы и не работают с py.test; многие одноразовые пакеты больше не разрабатываются и не поддерживаются.) - person Jérémie; 17.06.2019
comment
@ Jérémie Я согласен, вы хотите, чтобы я обновил ответ? - person Dmitry Mukhin; 21.06.2019
comment
Я завернул его в статическую функцию, которую я добавил в свое определение TestCase, под названием addTest (cls, id, func_name, params, description). Он работает хорошо, но тест должен быть добавлен в глобальном масштабе (а не в if name == main, чтобы участники тестирования могли загрузить модуль и также найти тесты. Например, : myclass.addTest (1, test_equal, (a, b), Тестовые значения a и b равны) Следующим шагом будет построение цикла вокруг этого и получение данных из списка. Не уверен, почему unittest не имеет этого функция уже. - person Brooke Wallace; 24.04.2020
comment
@BrookeWallace, это был демонстрационный код для ответа, опубликованного в 2008 году. Теперь, согласно другим высокопоставленным ответам, для этого есть лучшие инструменты, в том числе что-то в модульном тестировании. - person Dmitry Mukhin; 27.04.2020

Использование 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 -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)
person codeape    schedule 29.08.2008
comment
Это очень чистый способ динамического создания тестовых примеров. - person gaborous; 29.07.2015
comment
Но имейте в виду, что 'setup ()' не будет знать, какие переменные используются в качестве аргументов для уступки. На самом деле setup () не будет знать, какой тест запущен, или переменные, установленные внутри test_generator (). Это усложняет проверку работоспособности в setup () и является одной из причин, по которой некоторые люди предпочитают py.test. - person Scott Prive; 30.01.2016
comment
Это работает! но тогда вы не можете использовать unittest.TestCase и его семейства методов assert *. - person Shiplu Mokaddim; 27.03.2018
comment
Собственно, можете. Я просмотрел документы unittest, и это было добавлено в 3.4 (я обновил ответ с помощью примера кода). - person codeape; 04.04.2018
comment
Проголосовали за раздел обновления. Именно то, что мне нужно. :) - person Saurabh Shrivastava; 25.06.2018
comment
Я отредактировал ответ, теперь информация о модуле находится вверху, где и принадлежит. - person codeape; 25.06.2018
comment
Есть ли способ запустить версию unittest с помощью pytest, чтобы он запускал все случаи и не останавливался на первом неудачном параметре? - person kakk11; 24.04.2019
comment
Как упоминалось @ kakk11, этот ответ (и подтест в целом) не работает с pytest. Это известная проблема. Для этой работы существует активно разрабатываемый плагин: github.com/pytest-dev/pytest-subtests - person Jérémie; 17.06.2019
comment
есть две проблемы: 1) первый неудачный субтест приводит к тому, что последующие субтесты не запускаются; 2) setUp() и tearDown() не вызываются для субтестов - person V.K.; 07.09.2019
comment
О, да !! Спасибо, по крайней мере, я открываю параметризованные тесты без внешней библиотеки - person poloC; 22.07.2021

Эту проблему можно элегантно решить с помощью метаклассов:

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()
person Guy    schedule 01.01.2014
comment
очень хорошо, столкнулся с различными проблемами с setattr, это просто работает! - person OriginalCliche; 17.09.2014
comment
Это отлично сработало для меня с Selenium. В качестве примечания, в классе TestSequence вы можете определять статические методы, такие как setUp (self), is_element_present (self, how, what), ... tearDown (self). Размещение их ПОСЛЕ оператора метакласса = TestSequenceMeta, похоже, работает. - person Love and peace - Joe Codeswell; 09.09.2015
comment
ИМХО, это решение лучше, чем выбранное в качестве принятого. - person petroslamb; 18.01.2016
comment
Не могли бы вы прояснить, почему это не работает, если просто переопределить __new__ из TestSequence класса (и, таким образом, полностью пропустить метакласс)? - person petroslamb; 18.01.2016
comment
@petroslamb Метод __new__ в метаклассе вызывается при определении самого класса, а не при создании первого экземпляра. Я бы предположил, что этот метод динамического создания методов тестирования более совместим с интроспекцией, используемой unittest для определения количества тестов в классе (т.е. он может составить список тестов до того, как когда-либо создаст экземпляр этого класса). - person BillyBBone; 23.02.2016
comment
Для меня это работает лучше, чем принятый ответ: обнаружение теста не работает должным образом с другим ответом, но работает с этим. - person Stuart Axon; 29.04.2016
comment
Примечание: в python 3 измените это на: class TestSequence(unittest.TestCase, metaclass=TestSequenceMeta):[...] - person Mathieu_Du; 30.12.2016
comment
... а для поддержки Python 2 и 3 см. python-future.org/compatible_idioms.html #metaclasses - person Thibaud Colas; 16.02.2017
comment
Полезно отметить, что это работает только при определении __new__, но не __init__. Это работает намного лучше, чем принятый ответ IMO, потому что он будет работать, даже когда unittest импортируется или добавляется в набор тестов. - person cangers; 15.11.2018
comment
Не могли бы вы использовать dct вместо dict? Использование ключевых слов в качестве имен переменных сбивает с толку и чревато ошибками. - person npfoss; 26.11.2018
comment
Очень хорошо работал в ситуации, когда у меня было несколько регулярных выражений, и я знал, каким должен быть match.groupdict для каждого положительного / отрицательного случая. Мне это не понадобилось, но хорошо знать, что вы можете передавать произвольные параметры ключевого слова в функцию метакласса __new__. - person Nathan Chappell; 02.06.2020

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

person Bernhard    schedule 01.04.2015
comment
Лучшее решение, если вы используете Python 3.4 и выше. - person Max Malysh; 20.12.2015
comment
При использовании unittest2 это также доступно для Python 2.7. - person Bernhard; 25.03.2016
comment
Одно из основных различий между этим подходом и отдельными тестами заключается в том, что состояние теста не сбрасывается каждый раз. (То есть setUp() и tearDown() не запускаются между субтестами.) - person Kevin Christopher Henry; 09.04.2016
comment
@KevinChristopherHenry Да, но теоретически self.setUp() можно вызвать вручную из подтеста. Что касается tearDown, может быть достаточно его автоматического вызова в конце. - person Acumenus; 01.05.2017
comment
Я думаю, что это может быть мощным, если использовать в сочетании с подходом метакласса, описанным выше. - person Nathan Chappell; 02.06.2020

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

person Javier    schedule 07.05.2014
comment
Приведенный выше код не работает: TypeError: __init __ () принимает не более 2 аргументов (задано 4) - person max; 17.11.2015
comment
Добавлены нулевые значения по умолчанию для дополнительных параметров конструктора. - person Javier; 24.11.2015
comment
Я предпочитаю код параметризации носа в ответе @mojo, но для моих клиентов он слишком полезен, чтобы избежать дополнительной зависимости, поэтому Я воспользуюсь этим для них. - person sage; 19.03.2016
comment
Это решение было моим любимым на этой странице. И Nose, предложенный в текущем популярном ответе, и его вилка Nose2 предназначены только для обслуживания, и последний предлагает пользователям вместо этого попробовать pytest. Какой беспорядок - я буду придерживаться такого нативного подхода! - person Sean; 01.02.2018
comment
Единственный недостаток, который я обнаружил, заключается в том, что сторонние участники тестирования иногда не поддерживают этот механизм, поскольку не знали о нем. - person Javier; 01.02.2018
comment
бонус: возможность переопределить метод shortDescription для вывода, переданного в параметрах - person fun_vit; 14.02.2018
comment
Вы также можете использовать for p in и *p во время инициализации; он избегает инициализации каждого элемента и последующей передачи! - person Nishant; 21.03.2021

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

person Sergey Voronezhskiy    schedule 02.09.2014
comment
Я искал простой и понятный пример параметризации тестовых примеров с помощью py.test. Большое спасибо! - person timgeb; 25.03.2016
comment
@timgeb Рад помочь вам. Дополнительные примеры см. В теге py.test. Также я предлагаю использовать hamcrest для добавления сахара в ваши утверждения с помощью удобочитаемых мутчеров, которые можно изменить. , комбинированные или созданные по-своему. Кроме того, у нас есть allure-python, красивое создание отчетов для py.test - person Sergey Voronezhskiy; 25.03.2016
comment
Спасибо. Я только начал переходить с unittest на py.test. Раньше у меня были TestCase базовые классы, которые могли динамически создавать потомков с разными аргументами, которые они сохраняли как переменные класса ... что было немного громоздко. - person timgeb; 25.03.2016
comment
@timgeb Ага, ты прав. Самая убойная функция py.test - это yield_fixtures. Что может выполнить настройку, вернуть некоторые полезные данные в тест и после его завершения выполнить разборку. Светильники также можно параметризовать. - person Sergey Voronezhskiy; 25.03.2016

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

person Mykhaylo Kopytonenko    schedule 21.04.2015

Вам будет полезно попробовать библиотеку TestScenarios.

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

person bignose    schedule 01.05.2009

Также существует гипотеза, которая добавляет нечеткое тестирование или тестирование на основе свойств.

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

person Javier    schedule 24.11.2015
comment
Я не мог использовать макрос @given() внутри класса unittest. - person John Greene; 12.08.2018

Вы можете использовать плагин нос-иттр (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
person Maroun    schedule 02.12.2014
comment
Мне нравится этот подход, особенно поддерживаемый им уровень метода. - person Matt; 06.03.2015

Я использую метаклассы и декораторы для генерации тестов. Вы можете проверить мою реализацию 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
person Kirill Ermolov    schedule 01.07.2015

Это фактически то же самое, что parameterized, как упоминалось в предыдущем ответе, но характерно для 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

Пример использования:

class TestStuff(unittest.TestCase):
    @sub_test([
        dict(arg1='a', arg2='b'),
        dict(arg1='x', arg2='y'),
    ])
    def test_stuff(self, a, b):
        ...
person Eric Cousineau    schedule 19.06.2020

На днях я наткнулся на ParamUnittest при просмотре исходного кода. для радона (пример использования в репозитории GitHub). Он должен работать с другими фреймворками, расширяющими TestCase (например, Nose).

Вот пример:

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)
person Matt    schedule 06.03.2015

import unittest

def generator(test_class, a, b):
    def test(self):
        self.assertEqual(a, b)
    return test

def add_test_methods(test_class):
    # The 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)
person Arindam Roychowdhury    schedule 07.12.2016
comment
Незначительная проблема с вашей def add_test_methods функцией. Должно быть def _add_test_methods Я думаю - person Raychaser; 13.01.2017
comment
@Raychaser ... Вы правы ... Я исправил это, но не обновлял здесь .... Спасибо, что уловили это. - person Arindam Roychowdhury; 16.01.2017

Просто используйте метаклассы, как показано здесь;

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
person sleepycal    schedule 10.05.2015

У меня были проблемы с очень специфическим стилем параметризованных тестов. Все наши тесты 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.

В любом случае, я надеюсь, что это может помочь тем, кто хочет провести аналогичную «глобальную» параметризацию своих тестов!

person Danielle Weisz    schedule 22.04.2016

Вы можете использовать TestSuite и пользовательские 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)
person Max Malysh    schedule 20.12.2015
comment
Пока TestSuite работает, аргументы не передаются __init__ функции. - person jadelord; 02.08.2017

Я обнаружил, что это хорошо работает для моих целей, особенно если мне нужно сгенерировать тесты, которые немного различают процессы в сборе данных.

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

Класс TestGenerator может использоваться для создания различных наборов тестовых примеров, таких как TestCluster.

TestCluster можно рассматривать как реализацию интерфейса TestGenerator.

person bcdan    schedule 06.08.2019

Это решение работает с unittest и nose для Python 2 и 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()
person mop    schedule 06.09.2016
comment
Спасибо @ guillaume-jacquenot за обновленную версию ‹3! - person mop; 26.04.2020

У меня были проблемы с тем, чтобы заставить их работать для 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
person hhquark    schedule 25.01.2018

Ответы на основе метаклассов по-прежнему работают в Python 3, но вместо атрибута __metaclass__ необходимо использовать параметр metaclass, например:

class ExampleTestCase(TestCase,metaclass=DocTestMeta):
    pass
person Patrick Ohly    schedule 08.05.2017

Мета-программирование - это весело, но оно может мешать. Большинство решений здесь затрудняют:

  • выборочно запустить тест
  • укажите обратно на код с именем теста

Итак, мое первое предложение - следовать простому / явному пути (работает с любым тестовым бегуном):

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

Поскольку нам не следует повторяться, мое второе предложение основано на Ответ Хавьера: используйте тестирование на основе свойств. Библиотека гипотез:

  • более неумолимо коварен в создании тестовых примеров, чем мы, простые люди

  • предоставит простые примеры-счетчики

  • работает с любым тестовым раннером

  • имеет еще много интересных функций (статистика, дополнительный тестовый результат, ...)

    класс 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"))

Хорошо, у вас нет разных названий тестов. Но может быть вам просто необходимо:

  • описательное имя тестируемого свойства.
  • какой ввод приводит к отказу (пример фальсификации).

Более забавный пример

person YvesgereY    schedule 08.08.2017

Помимо 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()
person pptime    schedule 26.05.2016
comment
Ссылка не работает (DNS?): Хм. Нам не удается найти этот сайт. Мы не можем подключиться к серверу на сайте blog.livreuro.com. - person Peter Mortensen; 30.01.2021

Вот мое решение. Я считаю это полезным, когда:

  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
    
person S.Arora    schedule 02.08.2016
comment
Это не отвечает на вопрос о генерации тестов на лету. - person lenz; 04.01.2018

person    schedule
comment
вроде как потерял там форматирование. это действительно трудно читать в нынешнем виде - person Arturo; 06.12.2019
comment
Объяснение было бы в порядке. - person Peter Mortensen; 06.01.2021