How would you?

… in my opinion 🧐

Writing test for code can quickly go out of hand if there are a huge number of conditions in the code, which in general there is. I recently came across such a situation where my code was 200 lines and the tests I wrote for it were 400 lines 🙈. I know it was too much but I have this practice where in I want to have 100% of test coverage. This practice of mine had sometimes made me pull my hair and prolonged my task for a day or two but it had ensured that there are minimum bugs in my code (at least that’s what I like to think 😜).

I can’t post the code here but I will try to create a minimum working example.

Consider the following application,

example.png

In this example,

  • First get which environment we are running the app in?
  • Second, get the username and password for this environment
  • Using this username and password generate a token
  • Then using this token call the APP which does things…

Now to test this workflow, I will start writing unit tests. The first test is for setup_environment and then second for get_auth_token and the third test for call_the_app (please excuse my function names).

For all these functions I will mock different environment variables

import os
from unittest.mock import Mock, patch

import pytest

@patch.dict(os.environ, {"WHICH_ENV": "dummy", "USERNAME": "PEPE", "PASSWORD": "PASSWORD"})
def test_setup_environment():
	# test my code here.
	...

@patch.dict(os.environ, {"WHICH_ENV": "", "USERNAME": "", "PASSWORD": ""})
def test_setup_environment_failed():
	# test the failing code here.
	...

@patch.dict(os.environ, {"WHICH_ENV": "dev", "USERNAME": "PEPE", "PASSWORD": "PASSWORD123"})
# Imagine here that we had other environment variables as well.
def test_generate_auth_token():
	# test my code here.
	...

@patch.dict(os.environ, {"WHICH_ENV": "dev", "USERNAME": "PEPE", "PASSWORD": "PASSWORD123"})
# Imagine here that we had other environment variables as well.
def test_call_the_app():
	# test my code here.
	...

Now as you can see that I was continuously using the same line of code to decorate all the functions which is not good as I am a firm believer of DRY (Don’t repeat yourself). This habit combined with my obsession of testing 100% of my code, had me writing 1000 lines of code for testing a few functions. There was a lot of repetition, I didn’t like it but most of the time had to give up on DRY as I had other task to finish.

Today I had some time on my hand and wanted to see how could I refactor my current code (200 lines of code and almost 500 lines in test cases) to have less repetitions. I came across an answer on stackoverflow (which I don’t have the link for now) but the answer was to use monkeypatch

import os
from unittest.mock import Mock, patch

import pytest

@pytest.fixture(autouse=True)
def mock_environment_variables(monkeypatch):
	monkeypatch.setenv("WHICH_ENV", "dummy")
	monkeypatch.setenv("USERNAME", "PEPE")
	monkeypatch.setenv("PASSWORD", "PASSWORD")

def test_setup_environment():
	# test my code here.
	...

But what if we need to change the environment variable, it was easy when the function was decorated with patch.dict and I can easily change it at one place. Well it is possible in unset them, or better set them again for just a single function,

import os
from unittest.mock import Mock, patch

import pytest

@pytest.fixture(autouse=True)
def mock_environment_variables(monkeypatch):
	monkeypatch.setenv("WHICH_ENV", "dummy")
	monkeypatch.setenv("USERNAME", "PEPE")
	monkeypatch.setenv("PASSWORD", "PASSWORD")

def test_setup_environment():
	# test my code here.
	...

def test_setup_environment_failed(monkeypatch):
	monkeypatch.setenv("WHICH_ENV", "")
	monkeypatch.setenv("USERNAME", "")
	monkeypatch.setenv("PASSWORD", "")
	# test the failing code here.
	...

And you can go about testing your code normally afterwards not having to worry about the environment variables.