Warning!

This course was written with pytest-bdd version 3. When pytest-bdd updated to version 4, they introduced a backwards-incompatible change regarding "@given" decorators. You must now include a "fixture_target" parameter with the name of the method in order for other steps to use it as a fixture. The example project code is updated, but the videos and transcripts still show the old code.

Transcripted Summary

So far in this course we've learned the basics of Gerken Style Feature File Tests using the pytest-bdd framework.

And, to be honest, most of our examples so far have been toy examples. They've been simple things like adding and removing cucumbers from a fictitious cucumber basket.

And they've truly all been simply unit tests. Now, BDD can be used for unit tests but that's not truly where it shines. BDD is best for feature testing where you have real product features worth describing in good behavior. So, let's change gears a little bit in our course.

In this chapter, we'll look at writing REST API Tests using pytest-bdd.

The REST API we'll be using for these tests is the DuckDuckGo Instant Answer API.



Why did we choose this?

Simple: It's a public and reliable API that's fairly easy to use. You can access it at Duckduckgo.com/API.

Our test will also use the Requests package for Python.



Requests is one of the most popular, most downloaded and also most beloved Python packages available. It really makes calling REST APIs a cinch.

Here on the documentation page you can see just how easy and concise it is to use the Requests package. Everything is very intuitive.

Here's our feature file for the DuckDuckGo Instant Answer API tests.


# service.feature

Feature: DuckDuckGo Instant Answer API
  As an application developer,
  I want to get instant answers for search terms via a REST API,
  So that my app can get answers anywhere.

  Scenario Outline: Basic DuckDuckGo API Query
    Given the DuckDuckGo API is queried with "<phrase>"
    Then the response status code is "200"
    And the response contains results for "<phrase>"

    Examples: Animals
      | phrase   |
      | panda    |
      | python   |
      | platypus |

    Examples: Fruits
      | phrase     |
      | peach      |
      | pineapple  |
      | papaya     |

I have my Feature with my user story and I also have my scenario written as a scenario outline.

The scenario is for a basic DuckDuckGo API query — Given the DuckDuckGo API is queried with some parametrized search phrase then the response status code is 200 and the response contains results for whatever my search phrase was.

I've used a Scenario Outline so I can search a bunch of different phrases because from a data-driven perspective I would expect any sort of phrase to work for this basic behavior.

Notice how there are 2 Examples tables. This is totally permissible!

Another thing you might notice that's interesting is that there's no When step. And that's a little odd. We'll see why we look at the Step Definition Module.

The Step Definition Module for my “service” test is very similar to the other Step Definition Module we saw in previous chapters.


# test_service.py

"""
This module contains step definitions for service.feature.
It uses the requests package:
http://docs.python-requests.org/
"""

import requests 
from pytest_bdd import scenarios, given, then, parsers

# Shared Variables
DUCKDUCKGO_API = 'https://api.duckduckgo.com/'

# Scenarios
scenarios('../features/service.feature', example_converters=dict(phrase=str))

# Given Steps
@given('the DuckDuckGo API is queried with "<phrase>"')

def ddg_response(phrase):
    params = {'q': phrase, 'format': 'json'}
    response = requests.get(DUCKDUCKGO_API, params=params)
    return response

# Then Steps
@then('the response contains results for "<phrase>"')
def ddg_response_contents(ddg_response, phrase):
    # A more comprehensive test would check 'RelatedTopics' for matching phrases
    assert phrase.lower() == ddg_response.json()['Heading'].lower()

@then(parsers.parse('the response status code is "{code:d}"'))
def ddg_response_code(ddg_response, code):
    assert ddg_response.status_code == code

A couple of differences though.

  • Here I've imported the Requests package: import requests

  • And also, as we scroll down, you'll notice that I've separated the sections by comments just for readability.

I have a shared variable that stores my DuckDuckGo API.

And I've declared all of the scenarios from “service.feature” using my scenarios helper function.

The Given step is the DuckDuckGo API's queried with some parametrized phrase — as you recall this is from a Scenario Outline.

  • My step definition function then calls the Requests module and gets the response using a requests.get for my DuckDuckGo API, as well as the parameters that include “q” for the phrase.

  • The format I'm hard coding to be json.

  • Because this is a Given step it returns the response as a fixture.

The Then steps then validate aspects of that response object.

  • As you can see it declares that Given function as a fixture.

  • What I do is I assert that my phrase in lowercase is equal to my response heading from the json data in lowercase as well.

Validating my response code, I pass on the response code as a set of digits or number.

  • And I'm going to assert that the DuckDuckGo response object’s status code is equal to the code I expect.

Now, the reason why there's no When step comes down to how the fixtures work.

Only Given steps are allowed to be fixtures.

If you try to make this a When step as an action and then called the fixture here and here [in the Then steps], that just wouldn't work, the test would crash.

So, that's why our feature file Scenario Outline needs to have this step — Given the DuckDuckGo API is queried with "<phrase>" — which makes the request and uses the responses, that fixture return value, as a Given.

So, let's run this test now.

I'll run it through PyCharm and we can see all 6 tests passed.



That's pretty awesome.


# Tips for Using BDD Tests with REST API Testing

Now that we've seen a basic REST API test using pytest-bdd, I want to give some advice in general on using BDD-style testing and frameworks for REST API testing.

There's 2 ways you can go about this:

  • You can either specify your scenarios using very imperative steps, mechanically looking at response codes and different fields and header values and parameters; or

  • You can try to be more declarative and focus on more end-to-end behavior, such as you call this service and then you call that service and you expect this to be in this system and that to be in the database and describing in more natural language.

In my opinion, the latter scenario is much more preferable to write than the former scenario.

Not to say that one is more important than the other. But rather if you're only doing testing for individual requests, or you want to do more data driven style testing where you crank through lots and lots of combinations of input on single or double requests.

A framework like pytest-bdd or Cucumber or another BDD framework may not be the best avenue simply because individual requests and data driven testing is usually very programmatic and low level.

It may be more beneficial to write traditional pytest functions or to use frameworks like Python's Tavern or to even use something like Karate in order to write those tests.

You can still do it with pytest-bdd no problem. It's just something to think about.

As I said before, BDD testing really shines with feature level testing — especially with end to end testing for multiple services or WebUI.

And in our next chapter we'll look at how to do WebUI testing with pytest-bdd.



Resources