0

I'm trying to automatize API testing with pytest-bdd and gherkin feature files.

My folder structur looks like this

C:.
│   config.json
│   conftest.py
│   geckodriver.log
│   __init__.py

├───.vscode
│       settings.json
│
├───features
│       api.feature
│
├───step_defs
│   │   test_apis.py
│   │   __init__.py
 

I have following feature file:

api.feature

Feature: API Tests
As a User,
I want to have feedback from external APIs,
So that my correct user manual can be found.
    
Background:
    Given The user is authenticated on stage "var_stage" with tenant "var_tenant"

Scenario: Basic Api request
    Given I set the request to endpoint "var_endpoint"
    When I send a HTTP "get" request
    Then I expect http response with status code "var_status_code" and response type "json"

I want to test different APIs but want to ensure that the necessary authentication is done once before (see Brackground) different scenarios are executed. For this reason I have setup the authentication using session of the user for the stage and tenant specified in the feature file in conftest.py together with a fixture.

    conftest.py

    import pytest
    import requests
    from pytest_bdd import given, parsers
    
    
    @pytest.fixture
    def config(scope='session'):
    
        # Read the file
        with open('config.json') as config_file:
            config = json.load(config_file)
    
        # Return config so it can be used
        return config

    @pytest.fixture
    @given(parsers.parse('The user is authenticated on stage "{stage}" with tenant "{tenant}"'))
    def http_session(config, stage, tenant, scope = 'session'):
    
        login_url = "https://"+stage+"/"+tenant+"/public/login"
    
        payload = {
           'username': config['username'],
           'password': config['password']
        }
    
        session = requests.Session()
    
        response = session.post(login_url, data=payload, allow_redirects=True)

My other file looks like this:

    test_apis.py
    
    from pytest_bdd import scenarios, given, when, then, parsers
    
    scenarios('../features/api.feature')
    
    @given(parsers.parse('I set the request to endpoint "{endpoint}"'))
    def set_endpoint(config, endpoint):
    
        assert len(endpoint) > 0
    
        url = config['url'] + \
            endpoint
        config['url'] = url
    
    
    @when(parsers.parse('I send a HTTP "{http_type}" request'))
    def set_http_type(config, http_type):
    
        assert len(http_type) > 0
    
        config['http_type'] = http_type
    
    
    @then(parsers.parse('I expect http response with status code "{status_code}" and response type "{response_type}"'))
    def request_endpoint_statuscode(config, http_session,  status_code, response_type):
    
        assert len(status_code) > 0
        assert len(response_type) > 0
        
        headers = config['headers']
        url = config['url']
        http_type = config['http_type']
        status_code_respone = None
    
        if http_type == 'get':
            status_code_response = http_session.get(
                url, headers=headers, allow_redirects=True)
    
        elif http_type == 'post':
            status_code_response = http_session.post(
                url, headers=headers, allow_redirects=True)
    
        assert status_code_response == int(status_code)

Running this I receive following error

    @pytest.fixture
      @given(parsers.parse('The user is authenticated on stage "{stage}" with tenant "{tenant}"'))
      def http_session(config, stage, tenant, scope='session'):
    E       fixture 'stage' not found
    >       available fixtures: _pytest_bdd_example, browser, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, config, doctest_namespace, http_session, monkeypatch, pytestbdd_stepdef_given_I set brandcode to "{brandcode}", pytestbdd_stepdef_given_I set the request to endpoint "{endpoint}", pytestbdd_stepdef_given_The user is authenticated on stage "{stage}" with tenant "{tenant}", pytestbdd_stepdef_given_trace, pytestbdd_stepdef_then_I expect http response with status code "{status_code}" and response type "{response_type}", pytestbdd_stepdef_then_response content length shall be greater than "{length}", pytestbdd_stepdef_then_trace, pytestbdd_stepdef_when_I send a HTTP "{http_type}" request, pytestbdd_stepdef_when_trace, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
    >       use 'pytest --fixtures [testpath]' for help on them.

I guess that it has something to do with the feature file and how it is used together with pytest.fixtures. Using only configuration file without pytest-bdd in conftest worked fine for creation of the http session. However I want to be able to specify the stage and tenant (and possibly also credentials for a test user) within the feature file.

s kop
  • 196
  • 4
  • 18

1 Answers1

1

The error occurs because pytest tries to run http_session as a fixtures but cannot find the required parameters stage and tenant as fixtures before pytest-bdd even executes the step Given The user is authenticated on stage "var_stage" with tenant "var_tenant".

If you want to use a pytest-bdd step as fixture in subsequent steps, you have to adjust the @scenario decorator as described in the docs; remove the @fixture part and add target_fixture="<fixture-name>" to @scenario.

So your http_session function would look like the following after the adjustment:

@given(parsers.parse('The user is authenticated on stage "{stage}" with tenant "{tenant}"'), target_fixture="http_session")
def http_session(config, stage, tenant, scope = 'session'):
    # ...
mattefrank
  • 231
  • 1
  • 9
  • Thanks for the hint I noticed this already and solved this issue using a class to save the http session. Is there any advantage of doing it with target_fixture? – s kop Jun 12 '23 at 07:54
  • 1
    I assume with "*solved the issue using a class*" you mean you have a globally accessible class (e.g. accessible by a module variable). There are three advantages of using fixtures that come to my mind. 1. Pytest takes care of calling the teardown functionality for you 2. Dependencies between fixtures and Gherkin steps are immediately clear by looking at the step definition, since fixtures are "imported" by specifying them as function parameters 3. Consistency/Readability: You are already using fixtures in your code. Using a different approach in addition would reduce readability – mattefrank Jun 12 '23 at 14:53
  • One further question. While debugging I found out that the fixture is running for every scenario where it is used. I just want to run it once for the specified background (authentification only needs to be done once) . Should I create another pytest.fixture for storing the http_session of the background and access these in the scenarios instead of the target_fixture? – s kop Jun 13 '23 at 15:03
  • If you want to keep the background for documentation purposes to make clear that authentication is needed for each scenario, I would probably go with the approach you suggested. As scope for the `http_session` I would use `module` and I would remove `target_fixture` again from the Given step to avoid accidental usage of the wrong `http_session` fixture. – mattefrank Jun 14 '23 at 16:22