Testing against multiple environments like dev, staging, and production is a critical part of ensuring your application behaves consistently across its lifecycle. Pytest, with its flexible plugin architecture, can be a powerful tool for managing these multi-environment test runs.
Here’s how you can set up pytest to test against different environments:
Imagine you have a simple web application and you want to test its /health endpoint. Your environments might be defined by different base URLs.
# conftest.py
import pytest
def pytest_addoption(parser):
parser.addoption("--env", action="store", default="dev",
help="Environment to test against (dev, stage, prod)")
@pytest.fixture
def base_url(request):
env = request.config.getoption("--env")
if env == "dev":
return "http://localhost:5000"
elif env == "stage":
return "https://staging.example.com"
elif env == "prod":
return "https://example.com"
else:
pytest.fail(f"Unknown environment: {env}")
# test_app.py
import requests
def test_health_endpoint(base_url):
response = requests.get(f"{base_url}/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
To run these tests against the development environment, you’d execute:
pytest --env dev
For staging:
pytest --env stage
And for production:
pytest --env prod
This setup allows you to easily switch the target environment for your tests by simply passing a command-line argument. The pytest_addoption hook registers a new command-line option --env, and the base_url fixture uses this option to determine which URL to return.
But what if your tests need more than just a different base URL? Perhaps different API keys, database connection strings, or feature flags are required per environment. You can extend this pattern by creating more fixtures that depend on the environment.
# conftest.py (continued)
@pytest.fixture
def api_key(request):
env = request.config.getoption("--env")
if env == "dev":
return "DEV_SECRET_KEY"
elif env == "stage":
return "STAGING_SECRET_KEY"
elif env == "prod":
return "PROD_SECRET_KEY"
else:
pytest.fail(f"Unknown environment: {env}")
# test_app.py (continued)
def test_authenticated_resource(base_url, api_key):
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(f"{base_url}/api/resource", headers=headers)
assert response.status_code == 200
# ... further assertions on resource data
Now, when you run pytest --env prod, the api_key fixture will provide the production API key, ensuring your tests interact with the correct authentication mechanisms for that environment.
You can also use pytest’s parametrization to run the same test function against multiple environments in a single command. This is particularly useful for regression testing where you want to verify behavior across all your active environments.
# conftest.py (modified for parametrization)
import pytest
def pytest_addoption(parser):
parser.addoption("--envs", action="store", default="dev,stage,prod",
help="Comma-separated list of environments to test against (e.g., dev,stage,prod)")
@pytest.fixture(params=["dev", "stage", "prod"], ids=["dev", "stage", "prod"])
def environment(request):
# This fixture will be run once for each value in 'params'
return request.param
@pytest.fixture
def base_url(environment):
if environment == "dev":
return "http://localhost:5000"
elif environment == "stage":
return "https://staging.example.com"
elif environment == "prod":
return "https://example.com"
else:
pytest.fail(f"Unknown environment: {environment}")
# test_app.py (modified to use environment fixture)
import requests
def test_health_endpoint_across_envs(base_url):
response = requests.get(f"{base_url}/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
In this parametrized version, the environment fixture yields each environment name. The base_url fixture then consumes this environment fixture. Pytest, seeing that base_url depends on environment, will automatically run any test that uses base_url once for each value yielded by environment. The ids argument in pytest.fixture(params=..., ids=...) provides meaningful names for each test run in the output.
To run all tests against all configured environments (defined by the params in the fixture):
pytest
Or to specify a subset:
pytest -k "not prod" # Skips prod, runs dev and stage
pytest --env dev # If you revert to the single --env option, it overrides params
The real power comes from combining these approaches. You can have a --env option for a single run, and also use parametrization for running across multiple environments. If you use --env it will override the parametrized environment fixture for that specific run.
One subtle but crucial aspect of managing environment-specific configurations is how you handle sensitive credentials. While hardcoding them in fixtures as shown above is fine for demonstration, in a real-world scenario, you’d want to use environment variables, a secrets management system (like HashiCorp Vault or AWS Secrets Manager), or encrypted configuration files. The fixtures would then be responsible for retrieving these secrets securely based on the selected environment. For example, a prod environment might fetch secrets from a secure store, while dev might use local environment variables.
The next challenge you’ll likely encounter is managing complex test data that also varies by environment, such as different user accounts or product catalogs.