Generate test data with pytest fixtures

Generate test data with pytest fixtures

In a previous blog I already explained how I did API calls with the python requests library. I also looked at setting up a new test environment with pytest. This post is about another pytest feature. The problem I want to solve is to send data over a REST interface. I do want that in a very generic way so that the data is separated from the actual test itself.

Let’s create a simple testcase that puts data on the server with the REST API and fetches with a GET the same data again. I create a file test_restapi.py. In this file I put the following code:

import requests 

from requests.auth import HTTPBasicAuth 

URL = "https://my/api/call" 
PWD = "mypwd" 
USER = "theUser" 

def test_post_and_get_data(): 
    data= [ 
        { 
        'startDate': '2022-02-12', 
        'endDate': '2022-02-22', 
        } 
    ] 
 
    r = requests.post(URL, json=data, auth=HTTPBasicAuth(USER, PWD)) 
    assert r.status_code == 200 
    r= requests.get(URL, auth=HTTPBasicAuth(USER, PWD)) 
    assert r.status_code == 200 
    reply = r.json() 
    assert reply[0]["startDate"] == '2022-02-12'

The first test is a fact. This test has a problem though. If you run it two times, the data is already in our system the second time.

Before or after the test, we should clean up the sytem. I choose to delete the data in the beginning of the test. The reason I do it before the test is because if a test fails, there is no cleanup at the end. This means that I can debug the data after a failing test very easily. If the DEL call is at the end, it might happen that the data is deleted.

I added the delete call at the start of the test. The code looks now like this:

...

def prepare_data(): 
    requests.delete(URL , auth=HTTPBasicAuth(USER, PWD))

def test_post_and_get_data():
    prepare_data()
    data= [ 
        { 
        'startDate': '2022-02-12', 
        'endDate': '2022-02-22', 
        } 
    ]
    ...

The preparing of the data is the same for eacht test. We can make a pytest fixture from that function. The pytest documentation tells us that fixtures are initialize test functions. Fixtures can be like what is in other frameworks the setup and teardown functions. But they can do more than that.

We can make a fixture from the preparation of the data. Pytest has a decorator that makes a fixture from your functions.

import pytest 

@pytest.fixture 
def prepare_data(): 
    requests.delete(URL , auth=HTTPBasicAuth(USER, PWD))

def test_post_and_get_data(prepare_data):
    data= [ 
        { 
        'startDate': '2022-02-12', 
        'endDate': '2022-02-22', 
        } 
    ]
    ...

The test function has now an input parameter that is the same as the name of the fixture. The test function will now first call the prepare_data function because it is a fixture that is used in that test.

What happens if we want to verify different input data? Let’s say all different start dates. We can write different testcases. That is not good coding because we do not want to repeat code.

pytest can add parameters in a test with the parametrize decorator. Let’s try that.

@pytest.mark.parametrize("startDate", ["2022-02-12", "2022-01-12", "2020-02-12"]) 
def test_post_and_get_data(prepare_data, startDate): 
    data = [  
        { 'startDate': startDate, 
          'endDate': '2023-01-01' 

    ] 

   .... 

Our test function is now called three times. Each time with a different start date. That is what we need, no?

Fetch input data from a file

What if I want to fetch our start dates from a file that is on my disk? Can I use the parametrize decorator for that? I did not find it. I have found another solution. I can use a fixture to do this.

Let’s create a fixture from the start dates first to see what our code looks.

startdates_list = ["2022-02-12", "2022-01-12", "2020-02-12"] 

@pytest.fixture(params = startdates_list ) 
def start_date(): 
    return request.param 

def test_post_and_get_data(prepare_data, start_date): 
    data = [  
        { 'startDate': start_date, 
          'endDate': '2023-01-01' 
    ] 
    .... 

It is possible to parametrize fixtures. The fixture will be called for each parameter. This means that in our case the fixture will return the next value in the list and injects that in the testcase. The test is in this case also like in the example with the parametrize decorator.

Now that we have a list, it is very simple to use a generator. A generator and a list are iterators, so it should be possible. Let’s try it.

def startdates_generator(): 
    for d in  ["2022-02-12", "2022-01-12", "2020-02-12"]: 
        yield d 

@pytest.fixture(params = startdates_generator()) 
def start_date(): 
    return request.param 

def test_post_and_get_data(start_date): 
    data = [  
        { 'startDate': start_date, 
          'endDate': '2023-01-01' 

    ] 
    .... 

Yes, it works! The test is called 3 times with the correct start dates. It is now very easy to adapt the startdates_generator function to fetch data from an input file. The initial question is solved. We can generate test data from an input file with pytest fixtures.