pytest-bdd: pytest_bdd.exceptions.GivenAlreadyUsed with step arguments

From the readme:

Often it’s possible to reuse steps giving them a parameter(s). This allows to have single implementation and multiple use, so less code. Also opens the possibility to use same step twice in single scenario and with different arguments!

But this doesn’t seem to be the case for @given. My usecase is this feature file:

Feature: Going back and forward.
    Testing the :back/:forward commands.

    Scenario: Going back
        Given I open backforward/1.html
        And I open backforward/2.html
        When I run :back
        Then backforward/1.html should be loaded

and this python file:

import pytest_bdd as bdd

bdd.scenarios('.')


@bdd.given(bdd.parsers.parse("I set {sect} -> {opt} to {value}"))
def set_setting(quteproc, sect, opt, value):
    quteproc.set_setting(sect, opt, value)


@bdd.given(bdd.parsers.parse("I open {path}"))
def open_path(quteproc, path):
    quteproc.open_path(path)


@bdd.when(bdd.parsers.parse("I run {command}"))
def run_command(quteproc, command):
    quteproc.send_cmd(command)


@bdd.then(bdd.parsers.parse("{} should be loaded"))
def url_should_be_loaded(httpbin, path):
    requests = httpbin.get_requests()
    assert requests[-1] == [('GET', url)]

So opening those two pages is clearly “setup”, i.e. I think they both belong to Given ....

But I get:

pytest_bdd.exceptions.GivenAlreadyUsed: Fixture "open_path" that implements this "I open backforward/2.html" given step has been already used.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 48 (22 by maintainers)

Commits related to this issue

Most upvoted comments

Reviving this issue, I’m bumping into the same problem, testing a tree-like data structure:

Given there is a root with value X And there is a node with value Y And there is a node with value Z …

The second “And” step triggers that error.

Another case: Given I have a garage named “Total Express” And there is a car with license plate “123-678” and the make is Toyota And its fuel tank has 32 gallons And there is another car with license plate “456-333” and the make is Ford And its fuel tank has 12 gallons …

The “another” is superfluous, and I need to implement it separately, causing duplicate code. The “its fuel tank has … gallons” throws an error. Yes, I could merge the “fuel tank” into the “there is a car” rule, but I prefer to keep those separate so I can reuse the “there is a car” clause in other scenarios where the fuel tank does not matter.

So bottom line, this limitation is annoying. At the very least add a configuration to relax it (perhaps even as part of the “strict gherkin” relaxing". I don’t mind fixing this… would it be okay?

I encountered this problem as well.

I see there are various opinions on the topic, and I agree with @olegpidsadnyi that the given should be data.

I see it as:

  • given: the precondition (there is no action to do, this describes the state of the system)
  • when: action under test
  • then: expected post condition of the action

given being data is perfectly reasonable, and pytest gives a lot of flexibility on how to create that data.

The current behavior though makes it hard to describe a system which has a variable amount of data to create.

This is such a common need that pytest explicitly mentions it in it’s own documentation: https://docs.pytest.org/en/latest/fixture.html#factories-as-fixtures

In this case a fixture is used as producer of objects, because tests might require a variable number of objects, or objects created with different parameters.

This is a very simple example

Given Alice is in chat A
  And Bob is in chat B
  And chat A auto-sync to chat B is enabled
When  Alice writes a message in chat A
Then  Bob sees a message in chat B

While trying to address the problem, I modified pytest-bdd to allow given to be used as a factory. We can still access the data created by given with the fixture name in then and when, but we are getting a list which contains all the data created by the given, so that it can be referenced by the steps.

@given(parsers.parse("{user} is in chat {chat}"), aggregate=True)
def user_in_chat(user, chat):
    return chat, user

@when(parsers.parse("{user} writes \"{message}\" in chat {chat}"))
def user_writes_in_chat(user, chat, message, chat_service, user_in_chat):
    # user_in_chat contains all the returned result of the given
    # In our example, both (Alice, A) and (Bob, B)

If you think this is a solution that fits the spirit of the project, I can look into cleaning up the PR and submit it. At the moment it’s a fairly small code change, and it’s backward compatible with the current behavior.

I think this is a very powerful feature, which brings pytest-bdd closer to feature parity with behave, plus all the power of pytest.

@olegpidsadnyi I’ve given this some more thought and discussed with colleagues, and I understand the problem a bit better now, and the solution to it. Making Given steps always fixtures does indeed cause problems, and although it’s going to be additional work for me to fix up my tests, it is a “step” (pun intended) in the right direction, so I withdraw my request to roll back the changes. Thank you for your time and explanations.

Agreed, this is pretty annoying.

This is supported in cucumber-js, and I can attest to it making testing a lot easier and more fun.

Hello,

Could you consider to relax this constraint by using a parameter (like strict_gherkin=False)?

This is restriction is really painful. For instance: this step is to inject value in an automotive bus, different named signals have to be sen in the same scenario:

@given(parsers.parse("BUS signal [{can_signal}] is equal to [{can_value:d}]"))
def ensure_bus_signal_value_is(can_signal, can_value, bus, context):
    ensure_bus_signal_value(can_signal, can_value, bus, context)

The BDD scenario is:

  Scenario: AC Button Value When Pressed
    Given BUS signal [NHVACACLampReq] is equal to [1]
    And BUS signal [NHVACACPowerReq] is equal to [2.6]
    When  I press air conditioning button for [3]s
    Then  air conditioning is starting

For me, the steps Given BUS signal [NHVACACLampReq] is equal to [1] and Given BUS signal [NHVACACPowerReq] is equal to [2.6] are strictly different and should be authorized.

I’ve also encountered it right now where the most intuitive option was to reuse the given statement but I got this error and I need to make a hack to make it work. It could be nice if this will be supported in the near future 😃

I also came here and after reading the whole thread, the solution is not obvious. But I finally get how to make it work by respecting the fact that in pytest-bdd, given is data and will become a pytest fixture.

Maybe a new section could be added to the documentation with the following example (or your own).

Let’s say I want to test a cli application, I could want to write the following scenario

Scenario: display the help message
    Given the cli parameter my_command
    And the cli parameter --help
    When I execute the cli
    Then I see the help message

With the code:

@given(parsers.parse('the cli parameter {cli_param}'))
...

This will fail in pytest-bdd as the given method would be executed twice and is not allowed.

I can think of 2 ways to solve that.


First possibility, combine the 2 given statements together

Scenario: display the help message
    Given the cli parameters my_command, --help
    When I execute the cli
    Then I see the help message

with the code

@given(parsers.cfparse('the cli parameter {cli_param:str+}', extra_types=dict(str=str)))
...

Second possibility, convert the given statements into when statements. Once I got past the feeling it is wrong to have multiple when statements (which purists will probably say), it made my tests much easier to write

Scenario: display the help message
    Given a cli parameter list
    When I add the cli parameter my_command
    And I add the cli parameter --help
    And I execute the cli
    Then I see the help message
@given('a cli parameter list')
def cli_parameter_list():
...

@when(parsers.parse('I add the cli parameter {cli_param}'))
def add_cli_parameter(cli_parameter_list, cli_param):
...

I hope this can help other people getting stuck with this issue.

I bumped in the same conceptual problem.

This is my conclusion (for the records so who’s going to bump into it again might see it from another angle).

Pytest use fixtures, which are ‘data’. The various verbs in the BDD feature file are mapped into fixutures when using pytest-bdd.

Given Blog One And Blog Two

the expected result is:

  • the result for the first line will be an object (i.e. data) representing Blog One
  • he result for the fsecond line will be an object (i.e. data) representing Blog Two

and can be mapped into

@given(“Blog one”): def blog_one(): return BlogOne()

@given(“Blog two”): def blog_two(): return BlogTwo()

and this works.

But, since there is the possibility of using parsers, there is the strong temptation of parametrising things and create

@given(parse(‘Blog {name:s}’) def create_block(name): return Factory(name)

This turns the ‘given’ verb into a data-factory, not data. Which other BDD products manage (maybe?) this way. pytest-bdd cannot distinguish if we are talking about a data-factory fixture which returns data, or a data fixture itself.

One approach is to just consider fixtures as data. The other approach is to allow parametrization and high flexibility creating those data-factories. This is a matter of product’s pholosophy and maybe BDD ‘purity’.

The former avoids the misuse of the data associated to the verbs, since (AFAIK) the BDD feature syntax is a Product Owner (or whateve is called in the method you use) tool, not a generic testing tool.

The latter approach doesn’t associate necessarily a BDD verb to a data, but the BDD verb creates the data nevertheless: the data is stored for later retrieval somehow. There is association, but not strong coupling one-verb=one-data. This can lead to a diiferent use of BDD given-when-then verbs, into really program-like steps which could be seen as a distorsion of the BDD ideas.

Also, from the technical point of view, the latter is incompatible with the concept of fixtures in pytest (at least as they are used within pytest-bdd) and the fact that pytest-bdd strongly couples fixtures with verb’s data makes it not possible to have parametrised flexibility in it (aka generic data generation from rules, instead of fixed data points on which to work)

anyway, all this to say: don’t confuse data and data-factories. Both are cool, as long as we are aware those are different concepts. pytest-bdd doesn’t cope well with factories, and that’s probably good depending on how you use BDD

my 2p.

It is pytest-bdd, so the setup is done via fixtures

Maybe we can add to given() function a flag (for example, reusable=True/False or smth like this) and add to documentation a big warning that in this case given() function ceases to be a fixture and become as a plain function?

@smoustaj I don’t really understand your problem, but it seems you are using given section to take exercise. Given is 1 fact. You can’t repeat it, it is given. For example “Given my article has 5 comments”. If you repeat it 5 times do you expect 25 comments? Exercise must be taken in the When section.

There just was someone in the #pylib IRC channel who wanted to do the same (using Given for arbitrary setup steps, even if they are based on side-effects). They used freshen for nose before, and there that apparently worked without issues.

To be honest, I’d still also like to do the same - using Given/When/Then for setup/testing/teardown.

@olegpidsadnyi - would you be okay with a patch to turn that check off? I’m reopening this for now 😄