Passing Test Inputs into pytest

Someone recently asked me this question:

I’m developing a pytest project to test an API. How can I pass environment information into my tests? I need to run tests against different environments like DEV, TEST, and PROD. Each environment has a different URL and a unique set of users.

This is a common problem for automated test suites, not just in Python or pytest. Any information a test needs about the environment under test is called configuration metadata. URLs and user accounts are common configuration metadata values. Tests need to know what site to hit and how to authenticate.

Using config files with an environment variable

There are many ways to handle inputs like this. I like to create JSON files to store the configuration metadata for each environment. So, something like this:

  • dev.json
  • test.json
  • prod.json

Each one could look like this:

{
  "base_url": "http://my.site.com/",
  "username": "pandy",
  "password": "DandyAndySugarCandy"
}

The structure of each file must be the same so that tests can treat them interchangeably.

I like using JSON files because:

  • they are plain text files with a standard format
  • they are easy to diff
  • they store data hierarchically
  • Python’s standard json module turns them into dictionaries in 2 lines flat

Then, I create an environment variable to set the desired config file:

export TARGET_ENV=dev.json

In my pytest project, I write a fixture to get the config file path from this environment variable and then read that file as a dictionary:

import json
import os
import pytest

@pytest.fixture
def target_env(scope='session'):
  config_path = os.environ['TARGET_ENV']
  with open(config_path) as config_file:
    config_data = json.load(config_file)
  return config_data

I’ll put this fixture in a conftest.py file so all tests can share it. Since it uses session scope, pytest will execute it one time before all tests. Test functions can call it like this:

import requests

def test_api_get(target_env):
  url = target_env['base_url']
  creds = (target_env['username'], target_env['password'])
  response = requests.get(url, auth=creds)
  assert response.status_code == 200

Selecting the config file with a command line argument

If you don’t want to use environment variables to select the config file, you could instead create a custom pytest command line argument. Bas Dijkstra wrote an excellent article showing how to do this. Basically, you could add the following function to conftest.py to add the custom argument:

def pytest_addoption(parser):
  parser.addoption(
    '--target-env',
    action='store',
    default='dev.json',
    help='Path to the target environment config file')

Then, update the target_env fixture:

import json
import pytest

@pytest.fixture
def target_env():
  config_path = request.config.getoption('--target-env')
  with open(config_path) as config_file:
    config_data = json.load(config_file)
  return config_data

When running your tests, you would specify the config file path like this:

python -m pytest --target-env dev.json

Why bother with JSON files?

In theory, you could pass all inputs into your tests with pytest command line arguments or environment variables. You don’t need config files. However, I find that storing configuration metadata in files is much more convenient than setting a bunch of inputs each time I need to run my tests. In our example above, passing one value for the config file path is much easier than passing three different values for base URL, username, and password. Real-world test projects might need more inputs. Plus, configurations don’t change frequency, so it’s okay to save them in a file for repeated use. Just make sure to keep your config files safe if they have any secrets.

Validating inputs

Whenever reading inputs, it’s good practice to make sure their values are good. Otherwise, tests could crash! I like to add a few basic assertions as safety checks:

import json
import os
import pytest

@pytest.fixture
def target_env():
  config_path = request.config.getoption('--target-env')
  assert os.path.isfile(config_path)

  with open(config_path) as config_file:
    config_data = json.load(config_file)

  assert 'base_url' in config_data
  assert 'username' in config_data
  assert 'password' in config_data

  return config_data

Now, pytest will stop immediately if inputs are wrong.

2 comments

  1. Why not use a library that has all that, and then some?

    Personally I am fond of dynaconf. It supports multiple environments, reading configuration from various files and file types, using environment variables to override arbitrarily complex part of configuration, validation, integrates with Vault to get secrets in secure way…

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s