pytest-bdd
Pytest-bdd implements a subset of the [gherkin] language to enable automating project requirements testing and to facilitate behavioral driven development
Пример проекта
pip install pytest
pip install pytest-bdd
Структура проекта
[project root directory]
|‐‐ [product code packages]
|-- [test directories]
| |-- features
| | `-- *.feature
| `-- step_defs
| |-- __init__.py
| |-- conftest.py
| `-- test_*.py
`-- [pytest.ini|tox.ini|setup.cfg]
@web @duckduckgo
Feature: DuckDuckGo Web Browsing
As a web surfer,
I want to find information online,
So I can learn new things and get tasks done.
# The "@" annotations are tags
# One feature can have multiple scenarios
# The lines immediately after the feature title are just comments
Scenario: Basic DuckDuckGo Search
Given the DuckDuckGo home page is displayed
When the user searches for "panda"
Then results are shown for "panda"
Обратите внимание, что в файле feature допускается только одна feature.
import pytest
from pytest_bdd import scenarios, given, when, then, parsers
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
# Constants
DUCKDUCKGO_HOME = 'https://duckduckgo.com/'
# Scenarios
scenarios('../features/web.feature')
# Fixtures
@pytest.fixture
def browser():
b = webdriver.Firefox()
b.implicitly_wait(10)
yield b
b.quit()
# Given Steps
@given('the DuckDuckGo home page is displayed')
def ddg_home(browser):
browser.get(DUCKDUCKGO_HOME)
# When Steps
@when(parsers.parse('the user searches for "{phrase}"'))
def search_phrase(browser, phrase):
search_input = browser.find_element_by_id('search_form_input_homepage')
search_input.send_keys(phrase + Keys.RETURN)
# Then Steps
@then(parsers.parse('results are shown for "{phrase}"'))
def search_results(browser, phrase):
# Check search result list
# (A more comprehensive test would check results for matching phrases)
# (Check the list before the search phrase for correct implicit waiting)
links_div = browser.find_element_by_id('links')
assert len(links_div.find_elements_by_xpath('//div')) > 0
# Check search phrase
search_input = browser.find_element_by_id('search_form_input')
assert search_input.get_attribute('value') == phrase
launch: pytest tests/step_defs
Более подробный пример
Функции, декорированные как scenario, ведут себя как обычные тестовые функции и будут выполняться после всех шагов сценария. Рекомендуется по возможности помещать всю логику внутрь when, then и and.
Иногда приходится объявлять одни и те же установки или шаги разными именами для лучшей читабельности. Чтобы использовать одну и ту же пошаговую функцию с несколькими именами шагов, ее можно декорировать несколько раз. Указанные псевдонимы шагов являются независимыми и будут выполняться каждый раз при упоминании.
@given("I have an article")
@given("there's an article")
def article(author, target_fixture="article"):
return create_test_article(author=author)
Сами шаги можно использовать с разными параметрами повторно. При этом доступно несколько типов парсеров параметра шага:
- string (the default)
- parse (based on: pypi_parse) - предоставляет простой синтаксический анализатор, который заменяет регулярные выражения для параметров шага удобочитаемым синтаксисом, например
{param:Type}
. Синтаксис вдохновлен встроенной в Python функциейstring.format()
. Параметры шага должны использовать синтаксис именованных полей pypi_parse в определениях шага. Именованные поля извлекаются, дополнительно преобразуются типы, а затем используются в качестве аргументов функции шага. Поддерживает преобразования типов с помощью преобразователей типов, передаваемых через extra_types. - cfparse (extends: pypi_parse, based on: pypi_parse_type) - предоставляет расширенный синтаксический анализатор с поддержкой «поля кардинальности» (CF)
- re
# cparse example
from pytest_bdd import parsers
@given(
parsers.cfparse("there are {start:Number} cucumbers", extra_types={"Number": int}),
target_fixture="cucumbers",
)
def given_cucumbers(start):
return {"start": start, "eat": 0}
# for re
@given(
parsers.re(r"there are (?P<start>\d+) cucumbers"),
converters={"start": int},
target_fixture="cucumbers",
)
def given_cucumbers(start):
return {"start": start, "eat": 0}
Кроме того,можно имплементировать собственные парсеры.
Иногда нужен такой заданный шаг, который обязательно менял бы фикстуру только для определенного теста (сценария), а для остальных тестов он оставался бы нетронутым. Для этого в given декораторе существует специальный параметр target_fixture. В этом примере существующая фикстура foo будет переопределена шагом только для сценария, в котором она используется.
Feature: Target fixture
Scenario: Test given fixture injection
Given I have injecting given
Then foo should be "injected foo"
from pytest_bdd import given
@pytest.fixture
def foo():
return "foo"
@given("I have injecting given", target_fixture="foo")
def injecting_given():
return "injected foo"
@then('foo should be "injected foo"')
def foo_is_foo(foo):
assert foo == 'injected foo'
Как и Gherkin, pytest-bdd поддерживает многострочные шаги (также известные как строки документа). Но гораздо более чистым и мощным способом:
Feature: Multiline steps
Scenario: Multiline step using sub indentation
Given I have a step with:
Some
Extra
Lines
Then the text should be parsed with correct indentation
Шаг считается многострочным, если следующая(ые) строка(и) после первой строки имеет отступ относительно первой строки. Затем имя шага просто расширяется путем добавления дополнительных строк с символами новой строки. В приведенном выше примере имя заданного шага будет таким
'I have a step with:\nSome\nExtra\nLines'
. Использоать это можно так:
from pytest_bdd import given, then, scenario, parsers
scenarios("multiline.feature")
@given(parsers.parse("I have a step with:\n{content}"), target_fixture="text")
def given_text(content):
return content
@then("the text should be parsed with correct indentation")
def text_should_be_correct(text):
assert text == "Some\nExtra\nLines"
Если есть относительно большой набор файлов функций, вручную привязывать сценарии к тестам с помощью декоратора сценариев сложно. Конечно, при ручном подходе вы получаете все возможности, чтобы иметь возможность дополнительно параметризовать тест, дать тестовой функции красивое имя, задокументировать ее и т. д., но в большинстве случаев вам это не нужно. Вместо этого вы хотите рекурсивно автоматически связать все сценарии, найденные в папках функций, с помощью помощника сценариев. Вы можете передать несколько путей, и эти пути могут быть либо файлами функций, либо папками функций.
from pytest_bdd import scenarios
# pass multiple paths/files
scenarios('features', 'other_features/some.feature', 'some_other_features')
Кроме того, сценарий можно привязать и декоратором
from pytest_bdd import scenario, scenarios
@scenario('features/some.feature', 'Test something')
def test_something():
pass
# assume 'features' subfolder is in this file's directory
scenarios('features')
Сценарии могут быть параметризованы для охвата нескольких случаев. В Gherkin они называются Scenario Outlines, а шаблоны переменных записываются с использованием угловых скобок (например, <var_name>
).
# content of scenario_outlines.feature
Feature: Scenario outlines
Scenario Outline: Outlined given, when, then
Given there are <start> cucumbers
When I eat <eat> cucumbers
Then I should have <left> cucumbers
Examples:
| start | eat | left |
| 12 | 5 | 7 |
from pytest_bdd import scenarios, given, when, then, parsers
scenarios("scenario_outlines.feature")
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
return {"start": start, "eat": 0}
@when(parsers.parse("I eat {eat:d} cucumbers"))
def eat_cucumbers(cucumbers, eat):
cucumbers["eat"] += eat
@then(parsers.parse("I should have {left:d} cucumbers"))
def should_have_left_cucumbers(cucumbers, left):
assert cucumbers["start"] - cucumbers["eat"] == left
Организовать структуру тестов можно так:
features
│
├──frontend
│ │
│ └──auth
│ │
│ └──login.feature
└──backend
│
└──auth
│
└──login.feature
tests
│
└──functional
│
└──test_auth.py
│
└ """Authentication tests."""
from pytest_bdd import scenario
@scenario('frontend/auth/login.feature')
def test_logging_in_frontend():
pass
@scenario('backend/auth/login.feature')
def test_logging_in_backend():
pass
Cucumber использует теги как способ классификации ваших функций и сценариев, которые поддерживает pytest-bdd.
@login @backend
Feature: Login
@successful
Scenario: Successful login
Теперь это можно запустить так: pytest -m "backend and login and successful"
Маркеры функций и сценариев не отличаются от стандартных маркеров pytest, а символ @
автоматически удаляется, чтобы разрешить выражения селектора тестов. Если вы хотите, чтобы теги, связанные с bdd, отличались от других тестовых маркеров, используйте префикс, например bdd
. Обратите внимание: если вы используете pytest с параметром --strict
, все теги bdd, упомянутые в файлах функций, также должны быть в настройке маркеров конфигурации pytest.ini. Также для тегов используйте имена переменных, совместимые с python, т. е. начинайте не с числа, используйте только символы подчеркивания или буквенно-цифровые символы и т. д. Таким образом, вы можете безопасно использовать теги для фильтрации тестов. Вы можете настроить преобразование тегов в метки pytest, внедрив хук pytest_bdd_apply_tag
и вернув из него True:
def pytest_bdd_apply_tag(tag, function):
if tag == 'todo':
marker = pytest.mark.skip(reason="Not implemented yet")
marker(function)
return True
else:
# Fall back to the default behavior of pytest-bdd
return None
Первоначальные установки теста осуществляется с помощью Given. Несмотря на то, что эти шаги выполняются в обязательном порядке для применения возможных побочных эффектов, pytest-bdd пытается извлечь выгоду из фикстур PyTest, которые основаны на внедрении зависимостей и делают настройку более декларативной.
Feature: The power of PyTest
Scenario: Symbolic name across steps
Given I have a beautiful article
When I publish this article
And my article is published
@given("I have a beautiful article", target_fixture="article")
def article():
return Article(is_beautiful=True)
@when("I publish this article")
def publish_article(article):
article.publish()
@given("my article is published")
def published_article(article):
article.publish()
return article
pytest-bdd не зависит от глобального контекста и все установки теста можно определять в given.
Часто бывает так, что для охвата определенной функции вам потребуется несколько сценариев. И логично, что установка для этих сценариев будет иметь некоторые общие части. pytest-bdd реализует бекграунды для функций.
Feature: Multiple site support
Background:
Given a global administrator named "Greg"
And a blog named "Greg's anti-tax rants"
And a customer named "Wilson"
And a blog named "Expensive Therapy" owned by "Wilson"
Scenario: Wilson posts to his own blog
Given I am logged in as Wilson
When I try to post to "Expensive Therapy"
Then I should see "Your article was published."
Scenario: Greg posts to a client's blog
Given I am logged in as Greg
When I try to post to "Expensive Therapy"
Then I should see "Your article was published."
В разделе Background следует использовать только Given шаги. Шаги When и Then запрещены, потому что их цели связаны с действиями и потреблением результатов; что противоречит цели Background — подготовить систему к испытаниям или «привести систему в известное состояние», как это делает Given. Утверждение выше относится к строгому режиму Gherkin, который включен по умолчанию.
Steps и Given можно использовать повторно. Лучшее решение - определить реюзабельные фикстуры в conftest
# content of common_steps.feature
Scenario: All steps are declared in the conftest
Given I have a bar
Then bar should have value "bar"
# content of conftest.py
from pytest_bdd import given, then
@given("I have a bar", target_fixture="bar")
def bar():
return "bar"
@then('bar should have value "bar"')
def bar_is_bar(bar):
assert bar == "bar"
# content of test_common.py
@scenario("common_steps.feature", "All steps are declared in the conftest")
def test_conftest():
pass
Иногда у вас есть определения шагов, которые было бы гораздо проще автоматизировать, чем писать их вручную снова и снова. Это распространено, например, при использовании таких библиотек, как pytest-factoryboy, которые автоматически создают фикстуры. Написание определений шагов для каждой модели может стать утомительной задачей. По этой причине pytest-bdd позволяет автоматически генерировать определения шагов. Хитрость заключается в том, чтобы передать параметр stacklevel в Given, When и т.д.. Это даст им указание внедрить фикстуры шага в соответствующий модуль, а не просто вставить их в кадр вызывающего объекта. Пример тут
Смотри еще:
- документация
- репо
- [gherkin]
- [behave]
- [pytest]
- [тестирование]