1

I would like to define a scenario as follows:

Scenario: An erroneous operation
  Given some data
  And some more data
  When I perform an operation
  Then an exception is raised

Is there a good way to do this so that the when step isn't actually called until a pytest.raises context manager can be constructed for it? I could use a callable fixture, but that's going to pollute all other uses of the "I perform an operation" step.

asthasr
  • 9,125
  • 1
  • 29
  • 43
  • Could you clarify the question please ? You can always just wrap your operation in this way in your relevant test: ``` with pytest.raises(Exception): perform_operation(data, more_data) ``` – Yassine El Bouchaibi Dec 19 '22 at 18:38
  • @YassineElBouchaibi If the `When` step is defined generically, then `I perform an operation` should be unaware of whether it's going to succeed or fail. The question of whether an error _should occur_ is only known in the `Then` step -- at which point the `When` step is already executed. Is there a way to have `pytest-bdd` "consume" the `When` step as part of the `Then` step, so that `When` is only called in the context of `Then`? – asthasr Dec 19 '22 at 21:19
  • It sounds like you might be overcomplicating this in a way that will hurt test reliability and code maintainability? Tests should be deterministic (set a seed to deal with randomness). So you should know the expected outcome of each test case at dev time. Then write two tests: one test contains "ok" test cases, the other contains "bad" and makes use of the context manager. You could pass the expected outcome as a part of the parameterization of the test case (and add an `if` statement in your test function) but that needlessly obfuscates the intent of the test. – webelo Dec 21 '22 at 22:37
  • This is not randomness, it's the structure of a BDD test. The `When` step should be generic, not aware of whether it'll succeed or fail; it's equivalent to a function call. The `Then` step specifies the state after the `When` step has been run. The `When` step has no context for the `Then` step, so it can't special case ("if then_step expects failure, then use pytest.raises"). Similarly, the `Then` step doesn't control execution of the `When` step, so it can't wrap its execution conditionally. – asthasr Dec 22 '22 at 17:13

1 Answers1

0

I'm not sure that I understood your question correctly, but aren't you trying to achieve something like this?

  1. Before each When step we are checking if next Then step contains "is raised".

  2. If so, we mark this When step as "expected to fail".

  3. During needed When step execution we check corresponding flag and use pytest.raises method to handle it.

For first two steps I use pytest_bdd_before_step hook and request fixture. And for 3rd I just define some function handle_step right in test module. Of course you should move it somewhere else. This function requires step (which is just some defined function with your code) and request.node.step.expect_failure to decide whether to use pytest.raises or not.

As an option you can use callable fixture (requesting request fixture) to store this function and avoid using request.node.step.expect_failure in such keywords.

Also you can add functionality to define allowed exceptions and so on.

test_exception.py

import pytest
from pytest_bdd import then, when, scenario


@scenario("exc.feature", "Test expecting correct exception")
def test_1():
    pass


def handle_step(step, expect_failure):
    if expect_failure:
        pytest.raises(Exception, step)
    else:
        step()


@when("I perform an operation")
def operation_calling_exception(request):
    def step():
        # some code that causes an exception
        print(1 / 0)
    handle_step(step, request.node.step.expect_failure)


@then('an exception is raised')
def ex_raised():
    pass

exc.feature

Feature: Target fixture
    Scenario: Test expecting correct exception
        When I perform an operation
        Then an exception is raised

conftest.py

def pytest_bdd_before_step(request, feature, scenario, step, step_func):
    # set default `expect_failure` for step
    step.expect_failure = False

    # make step available from request fixture
    request.node.step = step

    # Only for `When` keywords
    if step.keyword == "When":
        # get current step position in scenario
        step_position = scenario.steps.index(step)

        # get following `Then` step
        then_step = next((s for s in scenario.steps[step_position:] if s.keyword == "Then"), None)

        if "is raised" in then_step.name:
            step.expect_failure = True
pL3b
  • 1,155
  • 1
  • 3
  • 18
  • I'll award the bounty here for effort, but unfortunately this level of hackery on the hook basically means: no, there's no way to do this [in an obvious, supported, and maintainable way]. – asthasr Dec 25 '22 at 14:33