pytest-bdd(2)- step 详解

216 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情

背景

看看 @given、@when、@when 的源码


def given(name, converters=None, target_fixture=None):
    """Given step decorator.

    :param name: Step name or a parser object.
    :param converters: Optional `dict` of the argument or parameter converters in form
                       {<param_name>: <converter function>}.
    :param target_fixture: Target fixture name to replace by steps definition function.

    :return: Decorator function for the step.
    """
    return _step_decorator(GIVEN, name, converters=converters, target_fixture=target_fixture)


def when(name, converters=None, target_fixture=None):
    """When step decorator.

    :param name: Step name or a parser object.
    :param converters: Optional `dict` of the argument or parameter converters in form
                       {<param_name>: <converter function>}.
    :param target_fixture: Target fixture name to replace by steps definition function.

    :return: Decorator function for the step.
    """
    return _step_decorator(WHEN, name, converters=converters, target_fixture=target_fixture)


def then(name, converters=None, target_fixture=None):
    """Then step decorator.

    :param name: Step name or a parser object.
    :param converters: Optional `dict` of the argument or parameter converters in form
                       {<param_name>: <converter function>}.
    :param target_fixture: Target fixture name to replace by steps definition function.

    :return: Decorator function for the step.
    """
    return _step_decorator(THEN, name, converters=converters, target_fixture=target_fixture)

类型

string(默认)

可以视为精确解析器,它不解析任何参数,根据提供的字符串精确匹配对应的步骤名称

parse(基于 pypi_parse)

  • 提供一个简单的解析器,用像{param:Type}可读语法替换步骤参数的正则表达式
  • 可以不提供类型 {username}
  • 也可以提供类型{username:s}
  • 这个类型和写 string.format("%s") 时,支持的类型一样,比如 %d、%s、%f

step fixtrue 会覆盖同名的 pytest fixture

fixture.feature

Feature: Target fixture
    Scenario: Test given fixture injection
        Given I have injecting given
        Then foo should be "injected foo"

test_fixture.py

import pytest
from pytest_bdd import given, then, scenarios


@pytest.fixture
def foo():
    print("pytest fixture")
    return "foo"


@given("I have injecting given", target_fixture="foo")
def injecting_given():
    print("step fixture")
    return "injected foo"


@then('foo should be "injected foo"')
def foo_is_foo(foo):
    assert foo == 'injected foo'

# 简化版,代替 @scenarios
scenarios("fixture.feature")

命令行运行

pytest -sq test_fixture.py

运行结果

step fixture
.
1 passed in 0.01s

@given 提供了一个 fixture,因为存在同名的 pytest.fixture,所以会把它覆盖掉

除了 @given,@when、@then 也可以提供 fixture 功能

比较常用在 HTTP 请求断言

request.feature

Feature: Blog
    Scenario: Deleting the article
        Given there is an article

        When I request the deletion of the article

        Then the request should be successful

test_request.py

import pytest
from pytest_bdd import scenarios, given, when, then

scenarios("request.feature")


@pytest.fixture
def http_client():
    import requests
    return requests.session()


@given("there is an article", target_fixture="article")
def there_is_an_article():
    return dict(uid=1, name="book name", pages=150)


@when("I request the deletion of the article", target_fixture="request_result")
def there_should_be_a_new_article(article, http_client):
    return http_client.delete(f"http://127.0.0.1:8080/articles/{article['uid']}")


@then("the request should be successful")
def article_is_published(request_result):
    # 拿到 when 的结果进行断言
    assert request_result.status_code != 200

Scenarios 快捷方式

理解为自动绑定,不再不需要 @scenario

from pytest_bdd import scenarios

# assume 'features' subfolder is in this file's directory
scenarios('features')

# assume a feature file
scenarios('steps.feature')

scenarios 的源码

def scenarios(*feature_paths, **kwargs):
    """Parse features from the paths and put all found scenarios in the caller module.

    :param *feature_paths: feature file paths to use for scenarios
    """
    caller_locals = get_caller_module_locals()
    caller_path = get_caller_module_path()

    features_base_dir = kwargs.get("features_base_dir")
    if features_base_dir is None:
        features_base_dir = get_features_base_dir(caller_path)
    ...
  • 可以看到支持传递多个 path
  • kwargs 支持传递 features_base_dir 关键字参数

传递多个 path

# 可以是文件夹路径,也可以是具体的某个 feature 文件路径
scenarios('features', 'other_features/some.feature', 'some_other_features')

Scenarios 快捷方式

理解为自动绑定,不再不需要 @scenario

from pytest_bdd import scenarios

# assume 'features' subfolder is in this file's directory
scenarios('features')

# assume a feature file
scenarios('steps.feature')

scenarios 的源码

def scenarios(*feature_paths, **kwargs):
    """Parse features from the paths and put all found scenarios in the caller module.

    :param *feature_paths: feature file paths to use for scenarios
    """
    caller_locals = get_caller_module_locals()
    caller_path = get_caller_module_path()

    features_base_dir = kwargs.get("features_base_dir")
    if features_base_dir is None:
        features_base_dir = get_features_base_dir(caller_path)
    ...
  • 可以看到支持传递多个 path
  • kwargs 支持传递 features_base_dir 关键字参数

传递多个 path

# 可以是文件夹路径,也可以是具体的某个 feature 文件路径
scenarios('features', 'other_features/some.feature', 'some_other_features')

指定 features_base_dir

会从这个指定的目录下去寻找对应的 feature

三种常见使用

# 指定当前目录
scenarios('steps.feature', 'fixture.feature', features_base_dir=".")

# 相对路径,相对于当前运行该文件的目录
# '/Users/pololuo/work/foris_work_learn/bdd/test/steps.feature'
scenarios('steps.feature', 'fixture.feature', features_base_dir="test")

# 绝对路径
# '/bdd/steps.feature'
scenarios('steps.feature', 'fixture.feature', features_base_dir="/bdd")

联合使用 @scenario 和 scenarios

  • @scenario 可以理解为手动绑定某个场景
  • scenarios 再自动绑定其他场景
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')

test_something 场景绑定将保持手动,在 features 文件夹中找到的其他场景将自动绑定

背景

以前写接口自动化,一般都会用到数据驱动,假设 bdd 在没有数据驱动的情况下会怎么写?

Feature: Eating
    Scenario: Eat 5 out of 12
        Given there are 12 cucumbers
        When I eat 5 cucumbers
        Then I should have 7 cucumbers

    Scenario: Eat 5 out of 20
        Given there are 20 cucumbers
        When I eat 5 cucumbers
        Then I should have 15 cucumbers

Given、When、Then 高度重复,只是数据不一致而已

如何解决数据参数化的问题?就是通过 outlines

Scenario Outline: Eating
  Given there are <start> cucumbers
  When I eat <eat> cucumbers
  Then I should have <left> cucumbers

  Examples:
    | start | eat | left |
    |  12   |  5  |  7   |
    |  20   |  5  |  15  |

outline 重点

  • Examples 的第一行不会被当做数据进行运行,从第二行开始读取数据并运行,一行代表一个测试场景
  • 通过 <占位符>来表示变量参数,它可以卸载 Given、When、Then 步骤中

实际 🌰

outline.feature

Scenario Outline: Eating
  Given there are <start> cucumbers
  When I eat <eat> cucumbers
  Then I should have <left> cucumbers

  Examples:
    | start | eat | left |
    |  12   |  5  |  7   |
    |  20   |  5  |  15  |

test_outline.py

from pytest_bdd import given, then, parsers, scenarios, when

scenarios('outline.feature')


@given(parsers.parse("there are {start} cucumbers"))
def get_cucumbers(start):
    print(f"there ara {start} cucumbers")
    return start


@when(parsers.parse("I eat {eat} cucumbers"))
def eat_cucumbers(eat):
    print(f"I eat {eat} cucumbers")
    return eat


@then(parsers.parse("I should have {left} cucumbers"))
def left_cucumbers(left, eat, start):
    print(f"I should have {left} cucumbers")
    assert int(left) == (int(start) - int(eat))
    print("=== end ===")

命令行运行

pytest -sq test_outline.py

运行结果

there ara 12 cucumbers
I eat 5 cucumbers
I should have 7 cucumbers
=== end ===
.there ara 20 cucumbers
I eat 5 cucumbers
I should have 15 cucumbers
=== end ===
.
2 passed in 0.01s

可看到,收集了两个测试用例