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\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\\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\\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 (?Pnot)?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[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"