I have a python class
class EnvironmentParser:
def __init__(self):
self.A = os.getenv('A', 'a') + ".json"
self.B = os.getenv('B', 'b') + ".json"
The purpose of this class is to have some default file identifiers (eg. a.json and b.json) but if the need arises, this should be changeable at runtime by running the python script with some set environment variables (the actual keys are different but I don't want to write production code here).
In another class, an instance of EnvironmentParser is passed as a constructor argument, and these file identifiers are read off from the instance variables. I have tried to unit test this as follows:
os.environ['A'] = 'herp'
os.environ['B'] = 'derp'
path = Path("some path here")
environment = EnvironmentParser()
folder = AdviceFolder(path, environment)
self.assertEqual(folder.file_ids['A'], 'herp.json')
self.assertEqual(folder.file_ids['B'], 'derp.json')
where folder.file_ids
is a dictionary
{'A': environment.A, 'B': environment.B}
However the asserts fail, apparantly, folder.file_ids['A']
is 'a.json'
as if the os.environ
lines weren't there.
I am surprised because as far as I am aware, os.getenv
reads from os.environ
, so the execution order should be
os.environ['A']
and os.environ['B']
are set to 'herp'
and 'derp'
respectively;'A'
and 'B'
keys from os.environ
, hence these values should be 'herp'
and 'derp'
respectively.'environment'
variable pointing to the just instantiated object of EnvironmentParser, which should thus have environment.A == 'herp'
and environment.B = 'derp'
.But evidently, this goes wrong somewhere and I can't point out where.
At any rate, if I want to have unit tests for both default values for getenv as well as manually set values, how can I do them at the same time? I could run the test again with externally set env vars, but then one of the two tests would always fail.
Reproducible example:
Create two python files:
example.py
-----------------------
import os
class EnvironmentParser:
def __init__(self):
self.A = os.getenv('A', 'a') + ".json"
self.B = os.getenv('B', 'b') + ".json"
class Example:
def __init__(self, environment: EnvironmentParser):
self.map = {'A': environment.A, 'B': environment.B}
test_example.py
-----------------------
import unittest
import os
from example import EnvironmentParser, Example
class TestExample(unittest.TestCase):
def test_example_with_default_values(self):
environment = EnvironmentParser()
example = Example(environment)
self.assertEqual(example.map['A'], 'a.json')
self.assertEqual(example.map['B'], 'b.json')
def test_example_with_custom_values(self):
os.environ['A'] = 'herp'
os.environ['B'] = 'derp'
environment = EnvironmentParser()
example = Example(environment)
self.assertEqual(example.map['A'], 'herp.json')
self.assertEqual(example.map['B'], 'derp.json')
if __name__ == '__main__':
unittest.main()
Actually, I was wrong before. It is the first test method that fails because for some reason the values A = 'herp' and B = 'derp' are already set even in the first test method.
Nonetheless, the problem exists that I can't seem to be able to simultaneously test default and nondefault values. I guess I can del
from os.environ
, but surely there is a better way?
What is going on here is that unit tests are run in lexicographic order. This means that even though test_example_with_custom_values()
is defined after test_example_with_default_values()
, it's run before it and the environment variables are set.
One way to manage this would be to use the approaches suggested in the link above, e.g. rename the methods test_1()
and test_2()
, or change the unittest.TestLoader.sortTestMethodsUsing
function to one that will sort meaningful names in your desired order.
However, in this case, I think it is preferable to not depend on the order and instead not leave environment variables set after the method which changes them, by using the unittest.mock.patch()
decorator:
patch()
acts as a function decorator, class decorator or a context manager. Inside the body of the function or with statement, the target is patched with a new object. When the function/with statement exits the patch is undone.
So your tests become:
import unittest
import os
from example import EnvironmentParser, Example
from unittest.mock import patch
class TestExample(unittest.TestCase):
def test_example_with_default_values(self):
environment = EnvironmentParser()
example = Example(environment)
self.assertEqual(example.map['A'], 'a.json')
self.assertEqual(example.map['B'], 'b.json')
@patch.dict(os.environ, {'A': 'herp', 'B': 'derp'})
def test_example_with_custom_values(self):
environment = EnvironmentParser()
example = Example(environment)
self.assertEqual(example.map['A'], 'herp.json')
self.assertEqual(example.map['B'], 'derp.json')
if __name__ == '__main__':
unittest.main()
This should run without errors now:
$ python test_example.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
If you like you can also add the @patch.dict(os.environ, {}, clear=True)
decorator to test_example_with_default_values()
to ensure it is run in a context with all environment variables cleared, though this isn't necessary.