持续创作,加速成长!这是我参与「掘金日新计划 · 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
复制代码
可看到,收集了两个测试用例