Хабрахабр

[Перевод] Python Testing с pytest. ГЛАВА 3 pytest Fixtures

Вернуться Дальше

Эта книга — недостающая глава, отсутствующая в каждой всеобъемлющей книге Python.

Frank Ruiz
Principal Site Reliability Engineer, Box, Inc.

6 и pytest 3. Примеры в этой книге написаны с использованием Python 3. pytest 3. 2. 6, 2. 2 поддерживает Python 2. 3+ 7 и Python 3.

Вам не нужно загружать исходный код, чтобы понять тестовый код; тестовый код представлен в удобной форме в примерах. Исходный код для проекта Tasks, а также для всех тестов, показанных в этой книге, доступен по ссылке на веб-странице книги в pragprog.com. Там же, на веб-странице книги есть ссылка для сообщений errata и дискуссионный форум. Но что бы следовать вместе с задачами проекта, или адаптировать примеры тестирования для проверки своего собственного проекта (руки у вас развязаны!), вы должны перейти на веб-страницу книги и скачать работу.

Под спойлером приведен список статей этой серии.

Оглавление

Fixtures — это функции, выполняемые pytest до (а иногда и после) фактических тестовых функций. Теперь, когда вы видели основы pytest, обратим наше внимание на фикстуры, которые необходимы для структурирования тестового кода практически для любой нетривиальной программной системы. Вы можете использовать Fixtures, чтобы получить набор данных для тестирования. Код в фикстуре может делать все, что вам необходимо. Fixtures также используются для получения данных для нескольких тестов. Вы можете использовать Fixtures, чтобы получить систему в известном состоянии перед запуском теста.

Вот простой пример фикстуры, который возвращает число:

ch3/test_fixtures.py

import pytest @pytest.fixture()
def some_data(): """Return answer to ultimate question.""" return 42 def test_some_data(some_data): """Use fixture return value in a test.""" assert some_data == 42

Когда вы включаете имя фикстуры в список параметров тестовой функции, pytest знает, как запустить её перед запуском теста. Декоратор @pytest.fixture() используется, чтобы сообщить pytest, что функция является фикстурой. Фикстуры могут выполнять работу, а могут возвращать данные в тестовую функцию.

pytest определит это и найдет фикстуру с таким названием. Тест test_some_data() имеет в качестве параметра имя фикстуры some_data. pytest будет искать в модуле теста фикстуру с таким именем. Наименование значимо в pytest. Он также будет искать в файле conftest.py, если не найдет его в этом.

Я использую fixture, fixture function, и fixture method взаимозаменяемо, чтобы ссылаться на функции @pytest.fixture(), описанные в этой главе. Прежде чем мы начнем наше исследование фикстур (и файла conftest.py), мне нужно рассмотреть тот факт, что термин fixture имеет много значений в сообществе программирования и тестирования и даже в сообществе Python. Функции Fixture часто настраивают или извлекают некоторые данные, с которыми может работать тест. Фикстура также может использоваться для обозначения ресурса, который ссылается функцией фикстуры. Например, сообщество Django часто использует фикстуру для обозначения некоторых исходных данных, которые загружаются в базу данных в начале приложения. Иногда эти данные считаются фикстурой.

Независимо от других смысловых значений, в pytest и в этой книге test fixtures относятся к механизму, который обеспечивает pytest, чтобы отделить код “подготовка к (getting ready for)” и “очистка после (cleaning up after)” от ваших тестовых функций.

Тем не менее, фикстуры в pytest отличаются от фикстур в Django и отличаются от процедур setup и teardown, обнаруженных в unittest и nose. pytest fixtures — одна из уникальных фишек, которые поднимают pytest над другими тестовыми средами, и являются причиной того, почему многие уважаемые люди переключаются на… и остаются с pytest. Как только вы получите хорошую ментальную модель того, как они работают, вам станет полегче. Есть много особенностей и нюансов если говорить о фикстурах. Тем не менее, вам нужно поиграться с ними некоторое время, чтобы въехать, поэтому давайте начнем.

Обмен Fixtures через conftest.py

Для проекта задач все фикстуры будут находиться в tasks_proj/tests/conftest.py. Можно поместить фикстуры в отдельные тестовые файлы, но для совместного использования фикстур в нескольких тестовых файлах лучше использовать файл conftest.py где-то в общем месте, централизованно для всех тестов.

Вы можете поместить fixtures в отдельные тестовые файлы, если вы хотите, чтобы fixture использовался только в тестах этого файле. Оттуда, fixtures могут быть разделены любым тестом. Если вы это сделаете, fixtures, определенные в этих низкоуровневых файлах conftest.py, будут доступны для тестов в этом каталоге и подкаталогах. Аналогично, вы можете иметь другие файлы conftest.py в подкаталогах каталога top tests. Поэтому использование всех наших инструментов в файле conftest.py в корне тестирования, tasks_proj/tests, имеет наибольший смысл. Однако до сих пор fixtures в проекте «Задачи» были предназначены для любого теста.

Не импортируйте conftest ни когда! Хотя conftest.py является модулем Python, он не должен импортироваться тестовыми файлами. 95. Файл conftest.py считывается pytest и считается локальным плагином, что станет понятно, когда мы начнем говорить о плагинах в главе 5 «Плагины» на стр. Затем давайте переработаем некоторые наши тесты для task_proj, чтобы правильно использовать фикстуры. Пока что считайте tests/conftest.py как место где мы можем поместить fixtures, для использования всеми тестами в каталоге тестов.

Использование Fixtures для Setup и Teardown

И мы должны убрать какие то записи в конце, если есть какая-то необходимость в очистке. Большинство тестов в проекте Tasks предполагают, что база данных Tasks уже настроена, запущена и готова. К счастью, большая часть этого позаботилась в коде задач с tasks.start_tasks_db(<directory to store db\>, 'tiny' or 'mongo') и tasks.stop_tasks_db(); нам просто требуется вызвать их в нужный момент, и ещё нам понадобится временный каталог. И возможно понадобится также отключиться от базы данных.

Мы можем использовать её для тестирования и не должны беспокоиться о очистке. К счастью, pytest включает в себя отличную фикстуру под названием tmpdir. (Не переживайте; мы разберем tmpdir и более подробно распишем его с помощью tmpdir_factory в разделе «Использование tmpdir и tmpdir_factory» на стр. Это не магия, просто хорошая практика кодирования от самых пытливых людей. 71.)

С учетом всех этих составляющих, эта фикстура работает замечательно:

ch3/a/tasks_proj/tests/conftest.py

import pytest
import tasks
from tasks import Task @pytest.fixture()
def tasks_db(tmpdir): """Подключение к БД перед тестами, отключение после.""" # Setup : start db tasks.start_tasks_db(str(tmpdir), 'tiny') yield # здесь происходит тестирование # Teardown : stop db tasks.stop_tasks_db()

Однако он реализует__str__, поэтому мы можем использовать str(), чтобы получить строку для передачи в start_tasks_db(). Значение tmpdir не является строкой-это объект, который представляет каталог. Пока мы все еще используем "tiny" для TinyDB.

Однако, если в функции есть yield, то там произойдёт остановка, контроль передастся тестам и выполняется следующая за yield строка после завершения тестов. Функция fixture запускается перед тестами, которые ее используют. Код после yield «teardown» будет выполняться независимо от того, что происходит во время тестов. Поэтому подумайте о коде над yield как о «setup», а о коде после yield как о «teardown». Но вы можете. Мы не возвращаем данные с выходом в этомй фикстуре.

Давайте изменим один из наших тестов tasks.add(), чтобы использовать эту фикстуру:

сh3/a/tasks_proj/tests/func/test_add.py

import pytest
import tasks
from tasks import Task def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) должен возвращать целое число.""" # GIVEN инициализированная БД задач # WHEN добавлена новая задача # THEN вернулся task_id типа int new_task = Task('do something') task_id = tasks.add(new_task) assert isinstance(task_id, int)

Мне нравится структурировать тесты в форматеGIVEN/WHEN/THEN (ДАНО/КОГДА/ПОСЛЕ), используя комментарии, особенно если это не очевидно из кода, что происходит. Основное изменение здесь заключается в том, что дополнительная фикстура в файле была удалена, и мы добавили tasks_db в список параметров теста. Надеюсь, GIVEN инициализированные задачи db помогут выяснить, почему tasks_db используется в качестве инструмента для теста. Я думаю, что это полезно в этом случае.

Убедитесь, что Tasks установлен

Если вы пропустили эту главу, обязательно установите задачи с cd code;pip install ./tasks_proj/. Мы все еще пишем тесты для проекта Tasks в этой главе, который был впервые установлен в главе 2.

Трассировка Fixture Execution с –setup-show

Если вы запустите тест из последнего раздела, вы не увидите, какие фикстуры запущены:

$ cd /path/to/code/
$ pip install ./tasks_proj/ # если он еще не установлен
$ cd /path/to/code/ch3/a/tasks_proj/tests/func
$ pytest -v test_add.py -k valid_id
===================== test session starts ======================
collected 3 items
test_add.py::test_add_returns_valid_id PASSED
====================== 2 tests deselected ======================
============ 1 passed, 2 deselected in 0.02 seconds ============

К счастью, pytest предоставляет такой флаг для командной строки, -- setup-show, который делает именно это: Когда я разрабатываю fixtures, мне необходимо видеть, что работает и когда.

$ pytest --setup-show test_add.py -k valid_id
============================= test session starts ============================= collected 3 items / 2 deselected test_add.py
SETUP S tmpdir_factory SETUP F tmpdir (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tmpdir) func/test_add.py::test_add_returns_valid_id (fixtures used: tasks_db, tmpdir, tmpdir_factory). TEARDOWN F tasks_db TEARDOWN F tmpdir
TEARDOWN S tmpdir_factory =================== 1 passed, 2 deselected in 0.18 seconds ====================

Начиная с test_add_returns_valid_id, вы видите, что tmpdir работал перед тестом. Наш тест находится посередине, а pytest обозначил часть SETUP и TEARDOWN для каждой фикстуры. По-видимому, tmpdir использует его как фикстуру. И до этого tmpdir_factory.

F для области действия и S для области сеанса. F и S перед именами фикстур указывают область. 56. Я расскажу о сфере действия в разделе «Спецификация областей(Scope) Fixture» на стр.

Использование Fixtures для Test Data

Вы можете вернуть всё что угодно. Fixtures являются отличным местом хранения данных для тестирования. Вот фикстура, возвращающая кортеж смешанного типа:

ch3/test_fixtures.py

@pytest.fixture()
def a_tuple(): """Вернуть что-нибудь более интересное""" return (1, 'foo', None, ) def test_a_tuple(a_tuple): """Demo the a_tuple fixture.""" assert a_tuple[3]['bar'] == 32

= 32), мы увидим, что произойдет, когда тест с фикстурой потерпит неудачу: Поскольку test_a_tuple() должен завершиться неудачей (23!

$ cd /path/to/code/ch3
$ pytest test_fixtures.py::test_a_tuple
============================= test session starts ============================= collected 1 item test_fixtures.py F [100%] ================================== FAILURES ===================================
________________________________ test_a_tuple _________________________________ a_tuple = (1, 'foo', None, {'bar': 23}) def test_a_tuple(a_tuple): """Demo the a_tuple fixture."""
> assert a_tuple[3]['bar'] == 32
E assert 23 == 32 test_fixtures.py:38: AssertionError
========================== 1 failed in 0.17 seconds ===========================

В случае проведения тестов фикстуры — это параметры для теста, поэтому о них сообщается с помощью трассировки стека. Вместе с разделом трассировки стека pytest отображает параметры значения функции, вызвавшей исключение или не прошедшей assert. Что произойдет, если assert (или exception) случиться в fixture?

$ pytest -v test_fixtures.py::test_other_data
============================= test session starts ============================= test_fixtures.py::test_other_data ERROR [100%] =================================== ERRORS ====================================
______________________ ERROR at setup of test_other_data ______________________ @pytest.fixture() def some_other_data(): """Raise an exception from fixture.""" x = 43
> assert x == 42
E assert 43 == 42 test_fixtures.py:21: AssertionError
=========================== 1 error in 0.13 seconds ===========================

Трассировка стека правильно показывает, что assert произошёл в функции фикстуры. Происходит пара вещей. Это серьёзное различие. Кроме того, test_other_data сообщается не как FAIL, а как ERROR. Если тест вдруг терпит неудачу, вы знаете, что сбой произошел в самом тесте, а не зависит от какой то фикстуры.

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

ch3/a/tasks_proj/tests/conftest.py

# Памятка об интерфейсе Task constructor
# Task(summary=None, owner=None, done=False, id=None)
# summary то что требуется
# owner и done являются необязательными
# id задается базой данных @pytest.fixture()
def tasks_just_a_few(): """Все резюме и владельцы уникальны.""" return ( Task('Write some code', 'Brian', True), Task("Code review Brian's code", 'Katie', False), Task('Fix what Brian did', 'Michelle', False)) @pytest.fixture()
def tasks_mult_per_owner(): """Несколько владельцев с несколькими задачами каждый.""" return ( Task('Make a cookie', 'Raphael'), Task('Use an emoji', 'Raphael'), Task('Move to Berlin', 'Raphael'), Task('Create', 'Michelle'), Task('Inspire', 'Michelle'), Task('Encourage', 'Michelle'), Task('Do a handstand', 'Daniel'), Task('Write some books', 'Daniel'), Task('Eat ice cream', 'Daniel'))

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

Использование Multiple Fixtures

И вы использовали tmpdir в нашем task_db fixture. Вы уже видели, что tmpdir использует tmpdir_factory. Давайте продолжим цепочку и добавим некоторые специализированные фикстуры для непустых баз проекта tasks:

ch3/a/tasks_proj/tests/conftest.py

@pytest.fixture()
def db_with_3_tasks(tasks_db, tasks_just_a_few): """Подключение БД с 3 задачами, все уникальны.""" for t in tasks_just_a_few: tasks.add(t) @pytest.fixture()
def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): """Подключение БД с 9 задачами, 3 owners, с 3 задачами у каждого.""" for t in tasks_mult_per_owner: tasks.add(t)

Набор данных используется для добавления задач в базу данных. Все эти fixtures включают две фикстуры в свой список параметров: tasks_db и набор данных. Теперь тесты могут использовать их, если вы хотите, чтобы тест начинался с непустой базы данных, например:

ch3/a/tasks_proj/tests/func/test_add.py

def test_add_increases_count(db_with_3_tasks): """Test tasks.add() должен повлиять на tasks.count().""" # GIVEN db с 3 задачами # WHEN добавляется еще одна задача tasks.add(Task('throw a party')) # THEN счетчик увеличивается на 1 assert tasks.count() == 4

Мне нравится использовать комментарии для GIVEN/WHEN/THEN и пытается протолкнуть как можно больше данных (GIVEN) в фикстуры по двум причинам. Это также демонстрирует одну из главных причин использования fixtures: чтобы сфокусировать тест на том, что вы на самом деле тестируете, а не на том, что вы должны были сделать, чтобы подготовиться к тесту. Во-вторых, assert или exception в фикстуре приводит к ошибке (ERROR), в то время как assert или exception в тестовой функции приводит к ошибке (FAIL). Во-первых, это делает тест более читаемым и, следовательно, более ремонтопригодным. Это просто сбивает с толку. Я не хочу, чтобы test_add_increases_count() отказал, если инициализация базы данных завершилась неудачно. Давайте запустим и посмотрим, как работают все фикстуры: Я хочу, чтобы сбой (FAIL) test_add_increases_count() был возможен только в том случае, если add () действительно не смог изменить счетчик.

$ cd /path/to/code/ch3/a/tasks_proj/tests/func
$ pytest --setup-show test_add.py::test_add_increases_count ============================= test session starts ============================= collected 1 item test_add.py
SETUP S tmpdir_factory SETUP F tmpdir (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tmpdir) SETUP F tasks_just_a_few SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) func/test_add.py::test_add_increases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_just_a_few, tmpdir, tmpdir_factory). TEARDOWN F db_with_3_tasks TEARDOWN F tasks_just_a_few TEARDOWN F tasks_db TEARDOWN F tmpdir
TEARDOWN S tmpdir_factory ========================== 1 passed in 0.20 seconds ===========================

Давайте разберем, что это. Получили снова кучу F-ов и S для функции и области сеанса.

Спецификация областей(Scope) Fixture

Параметр scope для @ pytest.fixture() может иметь значения функции, класса, модуля или сессии. Фикстуры включают в себя необязательный параметр под названием scope, который определяет, как часто фикстура получает setup и torndown. Настроки tasks_db и все фикстуры пока не определяют область. Scope по умолчанию — это функция. Таким образом, они являются функциональными фикстурами.

Ниже приведено краткое описание каждого значения Scope:

  • scope='function'

    Часть setup запускается перед каждым тестом с помощью fixture. Выполняется один раз для каждой функции теста. Это область используемая по умолчанию, если параметр scope не указан. Часть teardown запускается после каждого теста с использованием fixture.

  • scope='class'

    Выполняется один раз для каждого тестового класса, независимо от количества тестовых методов в классе.

  • scope='module'

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

  • scope='session'

    Все методы и функции тестирования, использующие фикстуру области сеанса, используют один вызов setup и teardown. Выполняется один раз за сеанс.

Вот как выглядят значения scope в действии:

ch3/test_scope.py

"""Demo fixture scope.""" import pytest @pytest.fixture(scope='function')
def func_scope(): """A function scope fixture.""" @pytest.fixture(scope='module')
def mod_scope(): """A module scope fixture.""" @pytest.fixture(scope='session')
def sess_scope(): """A session scope fixture.""" @pytest.fixture(scope='class')
def class_scope(): """A class scope fixture.""" def test_1(sess_scope, mod_scope, func_scope): """Тест с использованием сессий, модулей и функций.""" def test_2(sess_scope, mod_scope, func_scope): """Демонстрация более увлекательна со множеством тестов.""" @pytest.mark.usefixtures('class_scope')
class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again, multiple tests are more fun."""

Давайте используем --setup-show для демонстрации, что количество вызовов fixture и setup в паре с teardown выполняются в зависимости от области:

$ cd /path/to/code/ch3/
$ pytest --setup-show test_scope.py
============================= test session starts ============================= collected 4 items test_scope.py
SETUP S sess_scope SETUP M mod_scope SETUP F func_scope test_scope.py::test_1 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP F func_scope test_scope.py::test_2 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP C class_scope test_scope.py::TestSomething::()::test_3 (fixtures used: class_scope). test_scope.py::TestSomething::()::test_4 (fixtures used: class_scope). TEARDOWN C class_scope TEARDOWN M mod_scope
TEARDOWN S sess_scope ========================== 4 passed in 0.11 seconds ===========================

Теперь вы можете видеть не только F и S для функции и сеанса, но также C и M для класса и модуля.

Я знаю, что это очевидно из кода, но это важный момент, чтобы убедиться, что вы полностью грокаете (Прим переводчика: грокать — скорее всего автор имеет ввиду термин из романа Роберта Хайнлайна "Чужак в стране чужой". Область(scope) определяется с помощью фикстуры. Область(scope) задается в определении фикстуры, а не в месте её вызова. Приблизительное значение "глубоко и интуитивно понимать"). Тестовые функции, которые используют фикстуру, не контролируют, как часто устанавливается(SETUP) и срывается(TEARDOWN) фикстура.

Таким образом, function scope fixture может зависеть от других function scope fixture (по умолчанию и используется в проекте Tasks до сих пор). Фикстуры могут зависеть только от других фикстур из той же или более расширенной области(scope). function scope fixture также может зависеть от класса, модуля и фикстур области сеанса, но в обратном порядке — никогда.

Смена Scope для Tasks Project Fixtures

С учетом этих знаний о scope, давайте теперь изменим область действия некоторых фикстур проекта Task.

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

К счастью, это всего лишь одна строка изменения кода (ну, две, если вы считаете tmpdir->tmpdir_factory в списке параметров): Чтобы использовать что-то вроде tasks_db в качестве области сеанса, необходимо использовать tmpdir_factory, так как tmpdir является областью функции и tmpdir_factory является областью сеанса.

ch3/b/tasks_proj/tests/conftest.py

"""Define some fixtures to use in the project.""" import pytest
import tasks
from tasks import Task @pytest.fixture(scope='session')
def tasks_db_session(tmpdir_factory): """Connect to db before tests, disconnect after.""" temp_dir = tmpdir_factory.mktemp('temp') tasks.start_tasks_db(str(temp_dir), 'tiny') yield tasks.stop_tasks_db() @pytest.fixture()
def tasks_db(tasks_db_session): """An empty tasks db.""" tasks.delete_all()

Поскольку мы не изменили его название, ни одна из фикстур или тестов, которые уже включают его, не должен измениться. Здесь мы изменили tasks_db в зависимости от tasks_db_session, и мы удалили все записи, чтобы убедиться, что он пуст.

Один раз за сеанс достаточно: Фикстуры данных просто возвращают значение, поэтому действительно нет причин, чтобы они работали все время.

ch3/b/tasks_proj/tests/conftest.py

# Reminder of Task constructor interface
# Task(summary=None, owner=None, done=False, id=None)
# summary is required
# owner and done are optional
# id is set by database @pytest.fixture(scope='session')
def tasks_just_a_few(): """All summaries and owners are unique.""" return ( Task('Write some code', 'Brian', True), Task("Code review Brian's code", 'Katie', False), Task('Fix what Brian did', 'Michelle', False)) @pytest.fixture(scope='session')
def tasks_mult_per_owner(): """Several owners with several tasks each.""" return ( Task('Make a cookie', 'Raphael'), Task('Use an emoji', 'Raphael'), Task('Move to Berlin', 'Raphael'), Task('Create', 'Michelle'), Task('Inspire', 'Michelle'), Task('Encourage', 'Michelle'), Task('Do a handstand', 'Daniel'), Task('Write some books', 'Daniel'), Task('Eat ice cream', 'Daniel'))

Теперь давайте посмотрим, будут ли все эти изменения работать с нашими тестами:

$ cd /path/to/code/ch3/b/tasks_proj
$ pytest
===================== test session starts ======================
collected 55 items
tests/func/test_add.py ...
tests/func/test_add_variety.py ............................
tests/func/test_add_variety2.py ............
tests/func/test_api_exceptions.py .......
tests/func/test_unique_id.py .
tests/unit/test_task.py ....
================== 55 passed in 0.17 seconds ===================

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

$ pytest --setup-show tests/func/test_add.py
============================= test session starts =============================
platform win32 -- Python 3.6.5, pytest-3.9.3, py-1.7.0, pluggy-0.8.0
rootdir: c:\_BOOKS_\pytest_si\bopytest-code\code\ch3\b\tasks_proj\tests, inifile: pytest.ini
collected 3 items tests\func\test_add.py
SETUP S tmpdir_factory
SETUP S tasks_db_session (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tasks_db_session) func/test_add.py::test_add_returns_valid_id (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). TEARDOWN F tasks_db SETUP F tasks_db (fixtures used: tasks_db_session) func/test_add.py::test_added_task_has_id_set (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). TEARDOWN F tasks_db
SETUP S tasks_just_a_few SETUP F tasks_db (fixtures used: tasks_db_session) SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) func/test_add.py::test_add_increases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_db_session, tasks_just_a_few, tmpdir_factory). TEARDOWN F db_with_3_tasks TEARDOWN F tasks_db
TEARDOWN S tasks_db_session
TEARDOWN S tmpdir_factory
TEARDOWN S tasks_just_a_few ========================== 3 passed in 0.24 seconds ===========================

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

Specifying Fixtures with usefixtures

Кроме того, можно отметить тест или класс с помощью @pytest.mark.usefixtures('fixture1', 'fixture2'). До сих пор, если вы хотели, чтобы тест использовал фикстуру, то вы помещали её в список параметров. Это не особо имеет смысл делать с тестовыми функциями — это просто дольше набирать текст. usefixtures принимает строку, состоящую из списка фикстур, разделенных запятыми. Но это хорошо работает для тестовых классов:

ch3/test_scope.py

@pytest.mark.usefixtures('class_scope')
class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again, multiple tests are more fun."""

Единственное отличие состоит в том, что тест может использовать возвращаемое значение фикстуры, только если оно указано в списке параметров. Использование usefixtures почти то же самое, что указание имени фикстуры в списке параметров метода теста. Тест, использующий фикстуру из-за использования usefixtures, не может использовать возвращаемое значение фикстуры.

Использование autouse для Fixtures That Always Get Used (которые используются непрерывно)

Однако вы можете использовать autouse=True, чтобы фикстура работала постоянно. До сих пор в этой главе все фикстуры, используемые тестами, были обертками тестов (или использовали usefixtures для этого одного примера класса). Вот довольно надуманный пример: Это хорошо работает для кода, который вы хотите запустить в определенное время, но тесты на самом деле не зависят от состояния системы или данных из фикстуры.

ch3/test_autouse.py

"""Демонстрация autouse fixtures.""" import pytest
import time @pytest.fixture(autouse=True, scope='session')
def footer_session_scope(): """Сообщает время в конце session(сеанса).""" yield now = time.time() print('--') print('finished : {}'.format(time.strftime('%d %b %X', time.localtime(now)))) print('-----------------') @pytest.fixture(autouse=True)
def footer_function_scope(): """Сообщает продолжительность теста после каждой функции.""" start = time.time() yield stop = time.time() delta = stop - start print('\ntest duration : {:0.3} seconds'.format(delta)) def test_1(): """Имитирует длительный тестовый тест.""" time.sleep(1) def test_2(): """Имитирует немного более длительный тест.""" time.sleep(1.23)

Вот как всё это выглядит: Тут мы демонстрируем добавление время тестирования после каждого теста, а также дату и текущее время в конце сеанса.

$ cd /path/to/code/ch3
$ pytest -v -s test_autouse.py
===================== test session starts ======================
collected 2 items
test_autouse.py::test_1 PASSED
test duration : 1.0 seconds
test_autouse.py::test_2 PASSED
test duration : 1.24 seconds
--
finished : 25 Jul 16:18:27
-----------------
=================== 2 passed in 2.25 seconds ===================

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

В проекте Tasks я чувствовал, что важно сохранить возможность проверить, что произойдет, если мы попытаемся использовать функцию API до инициализации БД. Теперь, когда вы видели autouse в действии, возможно вас интересует, почему мы не использовали его для tasks_db в этой главе. Но мы не сможем это проверить, если принудительно инициализировать каждый тест. Это должно привести к соответствующему исключению.

Переименование Fixtures

Однако, pytest позволяет вам переименовывать фикстуры с параметром name в @pytest.fixture(): Название фикстур, перечисленные в списке параметров тестов и других фикстур, использующих их, обычно совпадает с именем функции фикстуры.

ch3/test_rename_fixture.py

"""Демонстрация fixture renaming.""" import pytest @pytest.fixture(name='lue')
def ultimate_answer_to_life_the_universe_and_everything(): """Возвращает окончательный ответ.""" return 42 def test_everything(lue): """Использует более короткое имя.""" assert lue == 42

Это имя даже появляется, если мы запускаем его с помощью --setup-show: Здесь lue теперь является именем fixture, а не fixture_with_a_name_much_longer_than_lue.

$ pytest --setup-show test_rename_fixture.py
======================== test session starts ========================
collected 1 items test_rename_fixture.py SETUP F lue test_rename_fixture.py::test_everything (fixtures used: lue). TEARDOWN F lue ===================== 1 passed in 0.01 seconds ======================

В нем перечислены все фикстуры, доступные для теста, в том числе те, которые были переименованы: Если вам нужно выяснить, где определен lue, следует добавить параметр pytest --fixtures и дать ему имя файла для теста.

$ pytest --fixtures test_rename_fixture.py
======================== test session starts =======================
... ------------------ fixtures defined from test_rename_fixture ------------------
lue Return ultimate answer. ================= no tests ran in 0.01 seconds =================

К счастью, фикстуры, которые мы определили, находятся внизу, вместе с тем, где они определены. Большая часть вывода не показана — там много чего. Давайте используем это в проекте «Tasks»: Мы можем использовать это, чтобы найти определение lue.

$ cd /path/to/code/ch3/b/tasks_proj
$ pytest --fixtures tests/func/test_add.py
======================== test session starts ========================
...
tmpdir_factory Return a TempdirFactory instance for the test session.
tmpdir Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. ----------------------- fixtures defined from conftest ------------------------ tasks_db An empty tasks db.
tasks_just_a_few All summaries and owners are unique.
tasks_mult_per_owner Several owners with several tasks each.
db_with_3_tasks Connected db with 3 tasks, all unique.
db_with_multi_per_owner Connected db with 9 tasks, 3 owners, all with 3 tasks.
tasks_db_session Connect to db before tests, disconnect after. =================== no tests ran in 0.01 seconds ====================

Все фикстуры из нашего conftest.py есть. Классно! И в нижней части встроенного списка находится tmpdir и tmpdir_factory, которые мы также использовали.

Параметризация Фикстур

42, мы параметризовали тесты. В [Parametrized Testing]Параметризованном тестировании, на стр. Мы по-прежнему используем наш список задач, список идентификаторов задач и функцию эквивалентности, как и раньше: Мы также можем параметризовать фикстуры.

ch3/b/tasks_proj/tests/func/test_add_variety2.py

"""Test the tasks.add() API function."""

import pytest
import tasks
from tasks import Task

tasks_to_try = (Task('sleep', done=True),
Task('wake', 'brian'),
Task('breathe', 'BRIAN', True),
Task('exercise', 'BrIaN', False))

task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)
for t in tasks_to_try]

def equivalent(t1, t2):
"""Check two tasks for equivalence."""
return ((t1.summary == t2.summary) and
(t1.owner == t2.owner) and
(t1.done == t2.done))

Но теперь, вместо параметризации теста, мы параметризуем фикстуру под названием a_task:

ch3/b/tasks_proj/tests/func/test_add_variety2.py

@pytest.fixture(params=tasks_to_try)
def a_task(request):
"""Без идентификаторов."""
return request.param

def test_add_a(tasks_db, a_task):
"""Использование фикстуры a_task (без ids)."""
task_id = tasks.add(a_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, a_task)

Вы узнаете больше в следующей главе. Запрос, указанный в параметре fixture, является другой встроенной фикстурой, представляющей вызывающее состояние фикстуры. Он имеет поле param, которое заполняется одним элементом из списка, назначенного params в @pytest.fixture(params=tasks_to_try).

Поскольку наш список задач состоит из четырех задач, фикстура будет вызываться четыре раза, а затем тест будет вызываться четыре раза: Элемент a_task довольно прост — он просто возвращает request.param в качестве значения для теста, используя его.

$ cd /path/to/code/ch3/b/tasks_proj/tests/func
$ pytest -v test_add_variety2.py::test_add_a
===================== test session starts ======================
collected 4 items
test_add_variety2.py::test_add_a[a_task0] PASSED
test_add_variety2.py::test_add_a[a_task1] PASSED
test_add_variety2.py::test_add_a[a_task2] PASSED
test_add_variety2.py::test_add_a[a_task3] PASSED
=================== 4 passed in 0.03 seconds ===================

Однако мы можем использовать тот же список строк, который мы использовали при параметризации наших тестов: Мы не предоставили идентификаторы, так pytest сам выдумал имена, добавив номер вызова(число) к имени фикстуры.

ch3/b/tasks_proj/tests/func/test_add_variety2.py

@pytest.fixture(params=tasks_to_try, ids=task_ids)
def b_task(request):
"""Использование списка идентификаторов."""
return request.param

def test_add_b(tasks_db, b_task):
"""Использование фикстуры b_task, с идентификаторами."""
task_id = tasks.add(b_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, b_task)

Этот вариант дает нам идентификаторы получше:

$ pytest -v test_add_variety2.py::test_add_b
===================== test session starts ======================
collected 4 items
test_add_variety2.py::test_add_b[Task(sleep,None,True)] PASSED
test_add_variety2.py::test_add_b[Task(wake,brian,False)] PASSED
test_add_variety2.py::test_add_b[Task(breathe,BRIAN,True)] PASSED
test_add_variety2.py::test_add_b[Task(exercise,BrIaN,False)] PASSED
=================== 4 passed in 0.04 seconds ===================

Вот как это выглядит, когда мы используем функцию для генерации идентификаторов: Мы также можем установить параметр ids в функцию, которую мы пишем, которая предоставляет идентификаторы.

ch3/b/tasks_proj/tests/func/test_add_variety2.py

def id_func(fixture_value):
"""Функция для генерации идентификаторов."""
t = fixture_value
return 'Task({},{},{})'.format(t.summary, t.owner, t.done)

@pytest.fixture(params=tasks_to_try, ids=id_func)
def c_task(request):
"""Использование функции (id_func) для генерации идентификаторов."""
return request.param

def test_add_c(tasks_db, c_task):
"""Использование фикстуры с сгенерированными идентификаторами."""
task_id = tasks.add(c_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, c_task)

Поскольку параметризация представляет собой список объектов Task, id_func() будет вызываться с объектом Task, что позволяет нам использовать методы доступа namedtuple для доступа к одному объекту Task для генерации идентификатора одного объекта Task за раз. Функция будет вызвана из значения каждого элемента из параметризации. Это немного чище, чем генерировать полный список раньше времени, и выглядит одинаково:

$ pytest -v test_add_variety2.py::test_add_c
===================== test session starts ======================
collected 4 items
test_add_variety2.py::test_add_c[Task(sleep,None,True)] PASSED
test_add_variety2.py::test_add_c[Task(wake,brian,False)] PASSED
test_add_variety2.py::test_add_c[Task(breathe,BRIAN,True)] PASSED
test_add_variety2.py::test_add_c[Task(exercise,BrIaN,False)] PASSED
=================== 4 passed in 0.04 seconds ===================

Но с параметризованными фикстурами каждая тестовая функция, использующая эту фикстуру, будет вызываться несколько раз. С параметризованными функциями вы можете запускать эту функцию несколько раз. И в этом сила, брат!

Параметризация Fixtures в Tasks Project

До сих пор мы использовали TinyDB для всех тестов. Теперь давайте посмотрим, как мы можем использовать параметризованные фикстуры в проекте Tasks. Поэтому любой код, который мы пишем, и любые тесты, которые мы пишем, должны работать как с TinyDB, так и с MongoDB. Но мы хотим, чтобы наши варианты оставались открытыми до конца проекта.

Решение (в коде), для которого используется база данных, изолируется от вызова start_tasks_db() в фикстуре tasks_db_session:

ch3/b/tasks_proj/tests/conftest.py

"""Определяем некоторые фикстуры для использования в проекте."""

import pytest
import tasks
from tasks import Task

@pytest.fixture(scope='session')
def tasks_db_session(tmpdir_factory):
"""Подключение к БД перед тестами, отключение после."""
temp_dir = tmpdir_factory.mktemp('temp')
tasks.start_tasks_db(str(temp_dir), 'tiny')
yield
tasks.stop_tasks_db()

@pytest.fixture()
def tasks_db(tasks_db_session):
"""Пустая база данных tasks."""
tasks.delete_all()

Он просто завершает переключение на подсистему, которая отвечает за остальные взаимодействия с базой данных: Параметр db_type в вызове start_tasks_db() не является магическим.

tasks_proj/src/tasks/api.py

def start_tasks_db(db_path, db_type): # type: (str, str) -None """Подключения функций API к БД.""" if not isinstance(db_path, string_types): raise TypeError('db_path must be a string') global _tasksdb if db_type == 'tiny': import tasks.tasksdb_tinydb _tasksdb = tasks.tasksdb_tinydb.start_tasks_db(db_path) elif db_type == 'mongo': import tasks.tasksdb_pymongo _tasksdb = tasks.tasksdb_pymongo.start_tasks_db(db_path) else: raise ValueError("db_type должен быть 'tiny' или 'mongo'")

Небольшая хитрость: Чтобы протестировать MongoDB, нам нужно запустить все тесты с db_type равным mongo.

ch3/c/tasks_proj/tests/conftest.py

import pytest import tasks from tasks import Task # @pytest.fixture(scope='session', params=['tiny',]) @pytest.fixture(scope='session', params=['tiny', 'mongo']) def tasks_db_session(tmpdir_factory, request): """Connect to db before tests, disconnect after.""" temp_dir = tmpdir_factory.mktemp('temp') tasks.start_tasks_db(str(temp_dir), request.param) yield # this is where the testing happens tasks.stop_tasks_db() @pytest.fixture() def tasks_db(tasks_db_session): """An empty tasks db.""" tasks.delete_all()

Ещё добавил request в список параметров temp_db и установил db_type в request.param вместо того, чтобы просто выбрать "tiny" или "mongo". Здесь я добавил params=['tiny',' mongo'] в фикстуру-декоратор.

И поскольку значения уже являются строками, это отлично работает. Если установить --verbose или флаг -v при запуске в pytest параметризованных тестов или параметризованных фикстур, pytest присваивает мена разным прогонам на основе значения параметризации.

Installing MongoDB

Лично я тестировал с изданием сообщества MongoDB, найденным тут https://www.mongodb.com/download-center. Чтобы отслеживать тестирование MongoDB, убедитесь, что установлены MongoDB и pymongo. Однако использование MongoDB не обязательно поддерживать всю остальную часть книги; он используется в этом примере и в примере отладчика в Главе 7. pymongo установливается с pip—pip install pymongo.

Вот что мы пока имеем:

$ cd /path/to/code/ch3/c/tasks_proj $ pip install pymongo $ pytest -v --tb=no ===================== test session starts ====================== collected 92 items test_add.py::test_add_returns_valid_id[tiny] PASSED test_add.py::test_added_task_has_id_set[tiny] PASSED test_add.py::test_add_increases_count[tiny] PASSED test_add_variety.py::test_add_1[tiny] PASSED test_add_variety.py::test_add_2[tiny-task0] PASSED test_add_variety.py::test_add_2[tiny-task1] PASSED ... test_add.py::test_add_returns_valid_id[mongo] FAILED test_add.py::test_added_task_has_id_set[mongo] FAILED test_add.py::test_add_increases_count[mongo] PASSED test_add_variety.py::test_add_1[mongo] FAILED test_add_variety.py::test_add_2[mongo-task0] FAILED ... ============= 42 failed, 50 passed in 4.94 seconds =============

Облом. Хм. Вы узнаете, как отладить это в pdb: Отладка тестовых сбоев, на стр. Похоже, нам нужно будет изрядно отладиться, прежде чем мы позволим кому-либо использовать версию Mongo. До тех пор мы будем использовать версию TinyDB. 125.

Упражнения

  1. Создать тестовый файл test_fixtures.py.
    2.Напишите несколько fixtures—functions данных с помощью декоратора @pytest.fixture(), которые будут возвращать некоторые данные. Возможно, список или словарь, или кортеж.
  2. Для каждой фикстуры напишите хотя бы одну тестовую функцию, которая её использует.
  3. Напишите два теста, которые используют одну и ту же фикстуру.
  4. Запустить pytest --setup-show test_fixtures.py. Все фикстуры работают перед каждым тестом?
  5. Добавьте scope= 'module' в фикстуру из упражнения 4.
  6. Повторно запустите pytest --setup-show test_fixtures.py. Что изменилось?
  7. Для фикстуры из упражнения 6 измените return <data> на yield <data>.
  8. Добавить операторы печати до и после yield.
  9. Запустите pytest -s -v test_fixtures.py. Имеет ли результат смысл?

Что дальше

Поскольку фикстуры настолько гибкие, я использую их в значительной степени, чтобы как можно больше настроить мои тесты на фикстуры. Реализация pytest fixture достаточно гибкая, чтобы использовать фикстуры, такие как building blocks, для создания тестового setup и teardown, а также для смены различных фрагментов системы (например, замена Mongo для TinyDB).

В следующей главе вы подробно рассмотрите встроенные (builtin) фикстуры. В этой главе вы рассмотрели фикстуры pytest, которые пишете сами, а также пару встроенных(builtin) фикстур tmpdir и tmpdir_factory.

Вернуться Дальше

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть