Warning!
This course was created with an older version of pytest-bdd
.
Over the years, updates to the framework introduced backwards-incompatible changes.
The example project code and transcripts are updated, but the videos still show the old code.
Please review the pytest-bdd
docs to see
specific syntax changes in newer versions.
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.
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.
Warning!
The example code shown here in the transcript is up-to-date and will work with pytest-bdd version 6. It matches the code in the example repository. The code shown in the video is from pytest-bdd version 3 and is outdated.
"""
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, parsers, given, then
DUCKDUCKGO_API = 'https://api.duckduckgo.com/'
scenarios('../features/service.feature')
CONVERTERS = {
'code': int,
'phrase': str,
}
@given(
parsers.parse('the DuckDuckGo API is queried with "{phrase}"'),
target_fixture='ddg_response',
converters=CONVERTERS)
def ddg_response(phrase):
params = {'q': phrase, 'format': 'json'}
response = requests.get(DUCKDUCKGO_API, params=params)
return response
@then(
parsers.parse('the response contains results for "{phrase}"'),
converters=CONVERTERS)
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}"'),
converters=CONVERTERS)
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.
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.
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
.