341 lines
11 KiB
Python
341 lines
11 KiB
Python
import textwrap
|
|
from typing import Any, Callable
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from pytest_bdd import given, parser, parsers, then, when
|
|
from pytest_bdd.utils import collect_dumped_objects
|
|
|
|
|
|
@pytest.mark.parametrize("step_fn, step_type", [(given, "given"), (when, "when"), (then, "then")])
|
|
def test_given_when_then_delegate_to_step(step_fn: Callable[..., Any], step_type: str) -> None:
|
|
"""Test that @given, @when, @then just delegate the work to @step(...).
|
|
This way we don't have to repeat integration tests for each step decorator.
|
|
"""
|
|
|
|
# Simple usage, just the step name
|
|
with mock.patch("pytest_bdd.steps.step", autospec=True) as step_mock:
|
|
step_fn("foo")
|
|
|
|
step_mock.assert_called_once_with("foo", type_=step_type, converters=None, target_fixture=None, stacklevel=1)
|
|
|
|
# Advanced usage: step parser, converters, target_fixture, ...
|
|
with mock.patch("pytest_bdd.steps.step", autospec=True) as step_mock:
|
|
parser = parsers.re(r"foo (?P<n>\d+)")
|
|
step_fn(parser, converters={"n": int}, target_fixture="foo_n", stacklevel=3)
|
|
|
|
step_mock.assert_called_once_with(
|
|
name=parser, type_=step_type, converters={"n": int}, target_fixture="foo_n", stacklevel=3
|
|
)
|
|
|
|
|
|
def test_step_function_multiple_target_fixtures(pytester):
|
|
pytester.makefile(
|
|
".feature",
|
|
target_fixture=textwrap.dedent(
|
|
"""\
|
|
Feature: Multiple target fixtures for step function
|
|
Scenario: A step can be decorated multiple times with different target fixtures
|
|
Given there is a foo with value "test foo"
|
|
And there is a bar with value "test bar"
|
|
Then foo should be "test foo"
|
|
And bar should be "test bar"
|
|
"""
|
|
),
|
|
)
|
|
pytester.makepyfile(
|
|
textwrap.dedent(
|
|
"""\
|
|
import pytest
|
|
from pytest_bdd import given, when, then, scenarios, parsers
|
|
from pytest_bdd.utils import dump_obj
|
|
|
|
scenarios("target_fixture.feature")
|
|
|
|
@given(parsers.parse('there is a foo with value "{value}"'), target_fixture="foo")
|
|
@given(parsers.parse('there is a bar with value "{value}"'), target_fixture="bar")
|
|
def _(value):
|
|
return value
|
|
|
|
@then(parsers.parse('foo should be "{expected_value}"'))
|
|
def _(foo, expected_value):
|
|
dump_obj(foo)
|
|
assert foo == expected_value
|
|
|
|
@then(parsers.parse('bar should be "{expected_value}"'))
|
|
def _(bar, expected_value):
|
|
dump_obj(bar)
|
|
assert bar == expected_value
|
|
"""
|
|
)
|
|
)
|
|
result = pytester.runpytest("-s")
|
|
result.assert_outcomes(passed=1)
|
|
|
|
[foo, bar] = collect_dumped_objects(result)
|
|
assert foo == "test foo"
|
|
assert bar == "test bar"
|
|
|
|
|
|
def test_step_functions_same_parser(pytester):
|
|
pytester.makefile(
|
|
".feature",
|
|
target_fixture=textwrap.dedent(
|
|
"""\
|
|
Feature: A feature
|
|
Scenario: A scenario
|
|
Given there is a foo with value "(?P<value>\\w+)"
|
|
And there is a foo with value "testfoo"
|
|
When pass
|
|
Then pass
|
|
"""
|
|
),
|
|
)
|
|
pytester.makepyfile(
|
|
textwrap.dedent(
|
|
"""\
|
|
import pytest
|
|
from pytest_bdd import given, when, then, scenarios, parsers
|
|
from pytest_bdd.utils import dump_obj
|
|
|
|
scenarios("target_fixture.feature")
|
|
|
|
STEP = r'there is a foo with value "(?P<value>\\w+)"'
|
|
|
|
@given(STEP)
|
|
def _():
|
|
dump_obj(('str',))
|
|
|
|
@given(parsers.re(STEP))
|
|
def _(value):
|
|
dump_obj(('re', value))
|
|
|
|
@when("pass")
|
|
@then("pass")
|
|
def _():
|
|
pass
|
|
"""
|
|
)
|
|
)
|
|
result = pytester.runpytest("-s")
|
|
result.assert_outcomes(passed=1)
|
|
|
|
[first_given, second_given] = collect_dumped_objects(result)
|
|
assert first_given == ("str",)
|
|
assert second_given == ("re", "testfoo")
|
|
|
|
|
|
def test_user_implements_a_step_generator(pytester):
|
|
"""Test advanced use cases, like the implementation of custom step generators."""
|
|
pytester.makefile(
|
|
".feature",
|
|
user_step_generator=textwrap.dedent(
|
|
"""\
|
|
Feature: A feature
|
|
Scenario: A scenario
|
|
Given I have 10 EUR
|
|
And the wallet is verified
|
|
And I have a wallet
|
|
When I pay 1 EUR
|
|
Then I should have 9 EUR in my wallet
|
|
"""
|
|
),
|
|
)
|
|
pytester.makepyfile(
|
|
textwrap.dedent(
|
|
"""\
|
|
import re
|
|
from dataclasses import dataclass, fields
|
|
|
|
import pytest
|
|
from pytest_bdd import given, when, then, scenarios, parsers
|
|
from pytest_bdd.utils import dump_obj
|
|
|
|
|
|
@dataclass
|
|
class Wallet:
|
|
verified: bool
|
|
|
|
amount_eur: int
|
|
amount_usd: int
|
|
amount_gbp: int
|
|
amount_jpy: int
|
|
|
|
def pay(self, amount: int, currency: str) -> None:
|
|
if not self.verified:
|
|
raise ValueError("Wallet account is not verified")
|
|
currency = currency.lower()
|
|
field = f"amount_{currency}"
|
|
setattr(self, field, getattr(self, field) - amount)
|
|
|
|
|
|
@pytest.fixture
|
|
def wallet__verified():
|
|
return False
|
|
|
|
|
|
@pytest.fixture
|
|
def wallet__amount_eur():
|
|
return 0
|
|
|
|
|
|
@pytest.fixture
|
|
def wallet__amount_usd():
|
|
return 0
|
|
|
|
|
|
@pytest.fixture
|
|
def wallet__amount_gbp():
|
|
return 0
|
|
|
|
|
|
@pytest.fixture
|
|
def wallet__amount_jpy():
|
|
return 0
|
|
|
|
|
|
@pytest.fixture()
|
|
def wallet(
|
|
wallet__verified,
|
|
wallet__amount_eur,
|
|
wallet__amount_usd,
|
|
wallet__amount_gbp,
|
|
wallet__amount_jpy,
|
|
):
|
|
return Wallet(
|
|
verified=wallet__verified,
|
|
amount_eur=wallet__amount_eur,
|
|
amount_usd=wallet__amount_usd,
|
|
amount_gbp=wallet__amount_gbp,
|
|
amount_jpy=wallet__amount_jpy,
|
|
)
|
|
|
|
|
|
def generate_wallet_steps(model_name="wallet", stacklevel=1):
|
|
stacklevel += 1
|
|
@given("I have a wallet", target_fixture=model_name, stacklevel=stacklevel)
|
|
def _(wallet):
|
|
return wallet
|
|
|
|
@given(
|
|
parsers.re(r"the wallet is (?P<negation>not)?verified"),
|
|
target_fixture=f"{model_name}__verified",
|
|
stacklevel=2,
|
|
)
|
|
def _(negation: str):
|
|
if negation:
|
|
return False
|
|
return True
|
|
|
|
# Generate steps for currency fields:
|
|
for field in fields(Wallet):
|
|
match = re.fullmatch(r"amount_(?P<currency>[a-z]{3})", field.name)
|
|
if not match:
|
|
continue
|
|
currency = match["currency"]
|
|
|
|
@given(
|
|
parsers.parse(f"I have {{value:d}} {currency.upper()}"),
|
|
target_fixture=f"{model_name}__amount_{currency}",
|
|
stacklevel=2,
|
|
)
|
|
def _(value: int, _currency=currency) -> int:
|
|
dump_obj(f"given {value} {_currency.upper()}")
|
|
return value
|
|
|
|
@when(
|
|
parsers.parse(f"I pay {{value:d}} {currency.upper()}"),
|
|
stacklevel=2,
|
|
)
|
|
def _(wallet: Wallet, value: int, _currency=currency) -> None:
|
|
dump_obj(f"pay {value} {_currency.upper()}")
|
|
wallet.pay(value, _currency)
|
|
|
|
@then(
|
|
parsers.parse(f"I should have {{value:d}} {currency.upper()} in my wallet"),
|
|
stacklevel=2,
|
|
)
|
|
def _(wallet: Wallet, value: int, _currency=currency) -> None:
|
|
dump_obj(f"assert {value} {_currency.upper()}")
|
|
assert getattr(wallet, f"amount_{_currency}") == value
|
|
|
|
generate_wallet_steps()
|
|
|
|
scenarios("user_step_generator.feature")
|
|
"""
|
|
)
|
|
)
|
|
result = pytester.runpytest("-s")
|
|
result.assert_outcomes(passed=1)
|
|
|
|
[given, pay, assert_] = collect_dumped_objects(result)
|
|
assert given == "given 10 EUR"
|
|
assert pay == "pay 1 EUR"
|
|
assert assert_ == "assert 9 EUR"
|
|
|
|
|
|
def test_step_catches_all(pytester):
|
|
"""Test that the @step(...) decorator works for all kind of steps."""
|
|
pytester.makefile(
|
|
".feature",
|
|
step_catches_all=textwrap.dedent(
|
|
"""\
|
|
Feature: A feature
|
|
Scenario: A scenario
|
|
Given foo
|
|
And foo parametrized 1
|
|
When foo
|
|
And foo parametrized 2
|
|
Then foo
|
|
And foo parametrized 3
|
|
"""
|
|
),
|
|
)
|
|
pytester.makepyfile(
|
|
textwrap.dedent(
|
|
"""\
|
|
import pytest
|
|
from pytest_bdd import step, scenarios, parsers
|
|
from pytest_bdd.utils import dump_obj
|
|
|
|
scenarios("step_catches_all.feature")
|
|
|
|
@step("foo")
|
|
def _():
|
|
dump_obj("foo")
|
|
|
|
@step(parsers.parse("foo parametrized {n:d}"))
|
|
def _(n):
|
|
dump_obj(("foo parametrized", n))
|
|
"""
|
|
)
|
|
)
|
|
result = pytester.runpytest("-s")
|
|
result.assert_outcomes(passed=1)
|
|
|
|
objects = collect_dumped_objects(result)
|
|
assert objects == ["foo", ("foo parametrized", 1), "foo", ("foo parametrized", 2), "foo", ("foo parametrized", 3)]
|
|
|
|
|
|
def test_step_name_is_cached():
|
|
"""Test that the step name is cached and not re-computed eache time."""
|
|
step = parser.Step(name="step name", type="given", indent=8, line_number=3, keyword="Given")
|
|
assert step.name == "step name"
|
|
|
|
# manipulate the step name directly and validate the cache value is still returned
|
|
step._name = "incorrect step name"
|
|
assert step.name == "step name"
|
|
|
|
# change the step name using the property and validate the cache has been invalidated
|
|
step.name = "new step name"
|
|
assert step.name == "new step name"
|
|
|
|
# manipulate the step lines and validate the cache value is still returned
|
|
step.lines.append("step line 1")
|
|
assert step.name == "new step name"
|
|
|
|
# add a step line and validate the cache has been invalidated
|
|
step.add_line("step line 2")
|
|
assert step.name == "new step name\nstep line 1\nstep line 2"
|