I wanted to write a test for this function that doesn’t take any inputs and uses a local constant variable. Here’s the function:
import os
from dotenv import load_dotenv
load_dotenv()
api_key = os.getenv("API_KEY")
def get_api_key():
if api_key:
print(f"Success getting API key: {api_key}")
return api_key
else:
print("Failed to get API key")
return None
I had a couple of options for testing this: either go with functional tests or class-based tests like unittest
. Since I personally prefer unittest
for my projects—it feels cleaner to me—I decided to write the tests using unittest
.
So, I started with a basic test:
import unittest
from config.config import get_api_key
import pytest
class TestConfig(unittest.TestCase):
@pytest.fixture
def set_key_api(monkeypatch):
monkeypatch.setenv("API_KEY", "1234")
yield
monkeypatch.delenv("API_KEY", raising=False)
def test_config_success_get_api_key(self, set_api_key):
api_key = get_api_key()
assert api_key == "1234"
def test_config_failed_get_api_key(self, monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
api_key = get_api_key()
assert api_key is False
Right off the bat, I hit a few errors. The first one was some weird character in my terminal that I couldn’t even display properly—it was some encoding issue or something. Long story short, I opened VS Code, cleaned it up, and moved on to debugging.
My Initial Mistakes
I made a couple of rookie mistakes right away:
- In the main function, if no API key is found, it returns
None
, but I mistakenly wroteFalse
in my test. - My imports were a bit messy and needed some tidying up.
The Main Issue
The real problem was this error: monkeypatch
, a built-in pytest fixture, cannot be directly injected into unittest
test methods. I’ll explain why in more detail at the end of this post.
Another issue was that our function uses a local variable (api_key
), and I wasn’t sure if you could even patch local variables—at least, I didn’t know how to do it. More on this later too!
Patching Options
We had two choices:
- Patch the entire
config
module, but that wouldn’t really count as a proper test. - Use
os.getenv("API_KEY")
directly inside the function.
Here’s the pros and cons of going with os.getenv
:
- Pros: Makes the code more testable and flexible.
- Cons: On a large scale, it could add overhead and hurt performance.
Since I was working on a small demo project, testability and flexibility were more important to me than performance, so I stuck with using os.getenv
in the function.
Here’s the function again for clarity:
import os
from dotenv import load_dotenv
load_dotenv()
def get_api_key():
api_key = os.getenv("API_KEY")
if api_key:
print(f"Success getting API key: {api_key}")
return api_key
else:
print("Failed to get API key")
return None
This approach solved the issue of patching local variables, but we still needed to figure out how to inject monkeypatch
(a pytest fixture) into unittest
.
Fixing the monkeypatch
Injection
To make this work, we need to:
- Attach the pytest fixture to the
unittest
class so it recognizes it. - Grant the necessary permissions to allow injection.
The solution was to create a setUp
method in the unittest
class, assign self.monkeypatch = monkeypatch
, and combine it with a pytest fixture using @pytest.fixture(autouse=True)
. Here’s how it looks:
@pytest.fixture(autouse=True)
def setUp(self, monkeypatch):
self.monkeypatch = monkeypatch
Now that monkeypatch
is configured on the class, we can use its setenv
and delenv
methods to patch the data we want in our tests.
Here’s the final test code:
import unittest
import pytest
from config.config import get_api_key
class TestConfig(unittest.TestCase):
"""Unit tests for the configuration module, specifically testing the get_api_key function."""
@pytest.fixture(autouse=True)
def setUp(self, monkeypatch):
"""Set up the test environment by initializing the monkeypatch fixture.
Args:
monkeypatch: Pytest fixture for modifying environment variables during testing.
"""
self.monkeypatch = monkeypatch
def test_config_success_get_api_key(self):
"""Test that get_api_key returns the correct API key when the environment variable is set."""
self.monkeypatch.setenv("API_KEY", "1234")
api_key = get_api_key()
self.assertEqual(api_key, "1234")
def test_config_failed_get_api_key(self):
"""Test that get_api_key returns None when the API_KEY environment variable is not set."""
self.monkeypatch.delenv("API_KEY", raising=False)
api_key = get_api_key()
self.assertEqual(api_key, None)
Why Can’t unittest
Directly Inject monkeypatch
or Pytest Fixtures?
Here’s the deal: unittest
is class-based and has its own execution style, while pytest is more functional. When you try to inject a pytest fixture (like monkeypatch
) into a unittest
method, it’s like speaking two different languages. The class-based structure of unittest
doesn’t know how to handle pytest’s fixture injection, and even pytest itself isn’t sure how to manage it.
Think of fixtures like function signatures. Pytest looks at a function’s signature to know what to inject, but unittest
doesn’t care about this because it has no concept of pytest fixtures.
Why Can’t You Patch Local Variables?
It’s pretty straightforward: local variables don’t exist in the function’s namespace until the function runs. By the time you try to patch them from outside, they’re not accessible because they’re scoped inside the function. In short, you can’t patch local variables because they’re locked away in the function’s scope and not exposed externally.
Related Links
- GitHub: github.com/PyRz-Tech/auto_uploader
- LinkedIn: linkedin.com/in/mohammadreza-mahdian-38304038a
- Twitter (X): @PyRzTech
Top comments (0)