Как реализовать параметризованные тесты в Python?

Python
Как реализовать параметризованные тесты в Python?

Ранее я обращался к серии статей о фреймворках для модульного тестирования, в которых были представлены три самых популярных фреймворка для тестирования Python: unittest, Nose/nose2 и pytest.

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

1. Что такое параметризованный тест?

Для обычного теста метод тестирования нужно запустить только один раз, в то время как для параметризованного теста может потребоваться передать ряд параметров для метода тестирования, а затем выполнить несколько тестов.

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

Параметризованный тест — это «управляемый данными тест», который проверяет разные параметры одного и того же метода, чтобы охватить все возможные ожидаемые результаты ветвления. Его тестовые данные могут быть отделены от тестового поведения, помещены в файл, базу данных или внешний носитель, а затем прочитаны тестовой программой.

2. Реализация идеи параметризованного теста?

Вообще говоря, метод тестирования — это наименьшая тестовая единица, и его функции должны быть как можно более атомарными и едиными.

Давайте сначала рассмотрим две идеи для реализации параметризованного тестирования: один заключается в написании тестового метода, который проходит через все параметры теста внутри него, а другой — в написании логики для обхода параметров вне тестового метода, а затем по очереди вызывается тестовый метод. метод.

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

Итак, как существующие фреймворки для тестирования решают эту проблему?

Все они используют декораторы, и основная идея заключается в следующем:Используйте исходный тестовый метод (например, test()) для создания нескольких новых тестовых методов (таких как test1(), test2()...) и назначайте им параметры по очереди.

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

3. Как использовать параметризованные тесты?

в стандартной библиотеке PythonunittestСамо параметризованное тестирование не поддерживает, для решения этой проблемы некоторые люди специально разработали две библиотеки: однаddt,одинparameterized.

ddt — это сокращение от «Data-Driven Tests». Типичное использование:

import unittest
from ddt import ddt,data,unpack

@ddt
class MyTest(unittest.TestCase):
    @data((3, 1), (-1, 0), (1.2, 1.0))
    @unpack
    def test_values(self, first, second):
        self.assertTrue(first > second)

unittest.main(verbosity=2)

Результат запуска следующий:

test_values_1__3__1_ (__main__.MyTest) ... ok
test_values_2___1__0_ (__main__.MyTest) ... FAIL
test_values_3__1_2__1_0_ (__main__.MyTest) ... ok

==================================================
FAIL: test_values_2___1__0_ (__main__.MyTest)
--------------------------------------------------
Traceback (most recent call last):
  File "C:\Python36\lib\site-packages\ddt.py", line 145, in wrapper
    return func(self, *args, **kwargs)
  File "C:/Users/pythoncat/PycharmProjects/study/testparam.py", line 9, in test_values
    self.assertTrue(first > second)
AssertionError: False is not true

----------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

Результат показывает, что есть 3 теста, и детализирует статус выполнения и информацию об ошибке утверждения.

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

В приведенном выше примере библиотека ddt использует три декоратора (@ddt, @data, @unpack), что действительно уродливо. Давайте взглянем на относительно лучшую параметризованную библиотеку:

import unittest
from parameterized import parameterized

class MyTest(unittest.TestCase):
    @parameterized.expand([(3,1), (-1,0), (1.5,1.0)])
    def test_values(self, first, second):
        self.assertTrue(first > second)

unittest.main(verbosity=2) 

Результаты теста следующие:

test_values_0 (__main__.MyTest) ... ok
test_values_1 (__main__.MyTest) ... FAIL
test_values_2 (__main__.MyTest) ... ok

=========================================
FAIL: test_values_1 (__main__.MyTest)
-----------------------------------------
Traceback (most recent call last):
  File "C:\Python36\lib\site-packages\parameterized\parameterized.py", line 518, in standalone_func
    return func(*(a + p.args), **p.kwargs)
  File "C:/Users/pythoncat/PycharmProjects/study/testparam.py", line 7, in test_values
    self.assertTrue(first > second)
AssertionError: False is not true

----------------------------------------
Ran 3 tests in 0.000s

FAILED (failures=1)

В этой библиотеке используется только декоратор @parameterized.expand, который намного чище на бумаге.

Напоминаем, что исходный метод тестирования исчез, его заменили тремя новыми методами тестирования, но правила именования новых методов отличаются от примера ddt.

После введения юниттеста, потом смотрим на дохлыхnoseи новорожденныйnose2. Фреймворк для носа проходит модульное тестирование с плагинами, и вышеописанное использование такое же.

Кроме того, Nose2 также предоставляет собственную параметризованную реализацию:

import unittest
from nose2.tools import params

@params(1, 2, 3)
def test_nums(num):
    assert num < 4

class Test(unittest.TestCase):
    @params((1, 2), (2, 3), (4, 5))
    def test_less_than(self, a, b):
    assert a < b

Наконец, давайте взглянем на фреймворк pytest, который реализует параметризованные тесты следующим образом:

import pytest

@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
    assert(first > second)

Результаты теста следующие:

==================== test session starts ====================
platform win32 -- Python 3.6.1, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: C:\Users\pythoncat\PycharmProjects\study collected 3 items

testparam.py .F
testparam.py:3 (test_values[-1-0])
first = -1, second = 0

    @pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
    def test_values(first, second):
>       assert(first > second)
E       assert -1 > 0

testparam.py:6: AssertionError
.                                                         [100%]

========================= FAILURES ==========================
_________________________ test_values[-1-0] _________________________

first = -1, second = 0

    @pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
    def test_values(first, second):
>       assert(first > second)
E       assert -1 > 0

testparam.py:6: AssertionError
===================== 1 failed, 2 passed in 0.08s =====================
Process finished with exit code 0

Я еще хочу напомнить всем, что pytest тоже изменился с одного на три, но мы не можем видеть информацию о методе с новым именем. Означает ли это, что он не генерирует новые методы тестирования? Или это просто скрытие информации о новом методе?

4. Окончательный вывод

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

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

В частности, как они превращают метод в несколько методов и привязывают каждый метод к соответствующему параметру? При реализации какие сложные проблемы нужно решить?

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