3 Simple Ways To Define Your Pytest Environment Variables With Examples
When writing Python code, you’ve probably used Environment Variables, at some point.
Whether in serverless lambda functions or stateful deployed apps.
Environment Variables are variables whose state is passed to the code or script at runtime.
Or variables whose state doesn’t change for execution (e.g. related to the system, user or network).
Although the best practices point to maintaining stateless code at all times, this is often not always possible.
Imagine deploying your code to a server and finding out it doesn’t work.
So how do you ensure that Environment Variables are correctly picked up by your code at run time?
Pytest environment variable tests help you test your code in a TDD (test-driven development) way using Pytest, a Unit Testing Wrapper build over theunittest
framework.
It provides an excellent interface for writing high-coverage test code.
In this article, we’ll look at a few ways to set and test environment variables using Pytest. Namely
- How To Set Environment Variables In Pytest
- 1. Use The
pytest-env
Package (TL;DR) - 2. Define Env Vars In The Test
- 3. Use Fixtures & Monkeypatch
- Other Ways (CLI)
- 1. Use The
This will ensure your code efficiently handles environment variables and raises appropriate errors if these variables are not defined.
Let’s get started, shall we?
How To Set Environment Variables In Pytest
Let’s look at a few ways to set Environment Variables in your Unit Tests.
1. Use The pytest-env
Package (TL;DR)
If you’re here for a quick answer, here it is.
You can use the pytest-env package and a pytest.ini
file to define Environment Variables your code needs at test run-time. If you’re not familiar with pytest-ini
files please check out this article for a brief practical overview.
Let’s take a look at the source code.
core.py
(Source Code)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import os
from dotenv import load_dotenv
load_dotenv()
def load_env_vars() -> str:
"""
Function to load and print environment variables
:return: String containing env vars
"""
account_id = os.getenv("ACCOUNT_ID")
api_endpoint = os.getenv("API_ENDPOINT")
deployment_stage = os.getenv("DEPLOYMENT_STAGE")
return f"The API endpoint `{api_endpoint}` has been deployed to " \
f"`{deployment_stage}` in Account # `{account_id}`"
The source code contains a very simple method load_env_vars
that loads 3 environment variables
ACCOUNT_ID
API_ENDPOINT
DEPLOYMENT_STAGE
from a .env
file using the python-dotenv package. This package helps us define environment variables in a .env
file and easily load them into our source code.
Next, if you navigate into the /tests/
folder you’ll see 3 unit tests, each using a different form of loading environment variables, along with a pytest.ini
file.
Important Note: Environment Variables defined in your pytest.ini
file take precedence over a .env
file or local environment during the test execution.
pytest.ini
1
2
3
4
5[pytest]
env =
ACCOUNT_ID=9876
DEPLOYMENT_STAGE=dev2
API_ENDPOINT=www.test_api_endpoint.com
test_env_variables_examples_pytest_env.py
1
2
3
4
5
6
7
8
9def test_load_env_vars_using_pytest_env() -> None:
"""
Using pytest_env and pytest.ini
:return: None
"""
expected_response = "The API endpoint `www.test_api_endpoint.com` " \
"has been deployed to `dev2` in Account # `9876`"
actual_response = load_env_vars()
assert actual_response == expected_response
In the above example, you can see how Pytest has automatically detected the 3 environment variables from our pytest.ini
file.
Which means no overriding, no mocking or patching, and no fixtures.
IMO, this is the simplest and a fairly reliable way to ensure isolation of the test environment.
Now let’s look at a few other ways to do this.
Note - To run the test_env_variables_examples_basic.py
test please comment out the contents in pytest.ini
otherwise it will overwrite the environment variables.
2. Define Env Vars In The Test
You can also straightaway define your env variables within the test.
test_env_variables_examples_basic.py
1
2
3
4
5
6
7
8
9
10
11
12def test_load_env_vars_define_in_test() -> None:
"""
Using os.environ
:return: None
"""
os.environ["DEPLOYMENT_STAGE"] = "dev"
os.environ["API_ENDPOINT"] = "https://api.TEST_URL1.com"
os.environ["ACCOUNT_ID"] = "123"
expected_response = "The API endpoint `https://api.TEST_URL1.com` " \
"has been deployed to `dev` in Account # `123`"
actual_response = load_env_vars()
assert actual_response == expected_response
In the above code example, the unit test overrides the environment variables within the Unit Test.
While easy to understand, this method is
- Ugly
- Not scalable (imagine you had 10–20 env variables)
- Not reusable (imagine you had 10–20 tests).
Although basic, it’s still a popular way for a few unit tests.
Use Fixtures & Monkeypatch
While this is a popular way of defining environment variables, I’m not a big fan.
Just like the previous method it requires lots of custom fixtures and patching which is fine for small-scale tests but cannot be extended to a huge # of tests.
Let’s look at how it’s done.
test_env_variables_examples_fixtures_monkeypatch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def deployment_stage_env_var() -> pytest.fixture():
os.environ["DEPLOYMENT_STAGE"] = "staging"
return os.environ["DEPLOYMENT_STAGE"]
def api_endpoint_env_var() -> pytest.fixture():
os.environ["API_ENDPOINT"] = "https://api.TEST_URL2.com"
return os.environ["API_ENDPOINT"]
def account_id_env_var() -> pytest.fixture():
os.environ["ACCOUNT_ID"] = "789"
return os.environ["ACCOUNT_ID"]
def test_load_env_vars_fixtures(deployment_stage_env_var,
api_endpoint_env_var,
account_id_env_var) -> None:
"""
Using Fixtures
:return: None
"""
expected_response = "The API endpoint `https://api.TEST_URL2.com` " \
"has been deployed to `staging` in Account # `789`"
actual_response = load_env_vars()
assert actual_response == expected_response
def test_load_env_vars_monkeypatch(monkeypatch) -> None:
"""
Using Monkeypatch
:return: None
"""
monkeypatch.setenv("DEPLOYMENT_STAGE", "prod")
monkeypatch.setenv("API_ENDPOINT", "https://api.TEST_URL3.com")
monkeypatch.setenv("ACCOUNT_ID", "321")
expected_response = "The API endpoint `https://api.TEST_URL3.com` " \
"has been deployed to `prod` in Account # `321`"
actual_response = load_env_vars()
assert actual_response == expected_response
In the above code, you can see we’ve defined a fixture for each of the environment variables.
You can also define the fixtures in Pytest Conftest as a best practice.
In the test test_load_env_vars_fixtures
we pass these fixtures as parameters and verify the asserted response.
The other test test_load_env_vars_monkeypatch
uses the monkeypatch.setenv
attribute to set each of the env variables.
Here’s a quick refresher on Pytest Monkeypatch.
While effective and convenient this may not be the cleanest, most scalable of approaches. Although it may work for some use cases.
Other Ways
Some other ways you can pass environment variables to your Pytest unit tests include via the CLI (one we didn’t talk about).
Conclusion
In this article, you’ve seen a few ways to define environment variables for your Unit Tests.
We looked at examples using the python-dotenv
library, defining env vars in the test, fixtures and monkeypatching.
While there is no one size fits all solution, my goal here was to present a few ways, so you can choose the one that best suits your use case.
For a personal side project, it probably doesn’t matter. But if you’re writing enterprise-level code, you must be mindful of security and best practices.
I hope you found this helpful. Any questions please hit me up.
Till the next time… Cheers!