DEV Community

Cover image for # Steps to Write Tests for a Function with No Input, Leading to Injecting `monkeypatch` and Pytest Fixtures into `unittest`
MohammadReza Mahdian
MohammadReza Mahdian

Posted on

# Steps to Write Tests for a Function with No Input, Leading to Injecting `monkeypatch` and Pytest Fixtures into `unittest`

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
Enter fullscreen mode Exit fullscreen mode

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   
Enter fullscreen mode Exit fullscreen mode

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:

  1. In the main function, if no API key is found, it returns None, but I mistakenly wrote False in my test.
  2. 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:

  1. Patch the entire config module, but that wouldn’t really count as a proper test.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Attach the pytest fixture to the unittest class so it recognizes it.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)