Os.environ и os.getenv() странно взаимодействуют в модульном тесте

У меня есть класс Python

class EnvironmentParser:
    def __init__(self):
        self.A = os.getenv('A', 'a') + ".json"
        self.B = os.getenv('B', 'b') + ".json"

Целью этого класса является наличие некоторых идентификаторов файлов по умолчанию (например, a.json и b.json), но если возникнет необходимость, их следует изменить во время выполнения, запустив скрипт Python с некоторыми установленными переменными среды (фактические ключи: другое, но я не хочу писать здесь производственный код).

В другом классе экземпляр EnvironmentParser передается в качестве аргумента конструктора, и эти идентификаторы файлов считываются из переменных экземпляра. Я попытался выполнить модульное тестирование следующим образом:

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')

где folder.file_ids словарь

{'A': environment.A, 'B': environment.B}

Однако утверждения терпят неудачу, по-видимому, folder.file_ids['A'] - это 'a.json', как если бы строк os.environ не было.

Я удивлен, потому что, насколько мне известно, os.getenv читается из os.environ, поэтому порядок выполнения должен быть

  1. os.environ['A'] и os.environ['B'] установлены на 'herp' и 'derp' соответственно;
  2. создается экземпляр класса EnvironmentParser, поэтому при создании экземпляра он запрашивает ключи 'A' и 'B' у os.environ, следовательно, эти значения должны быть 'herp' и 'derp' соответственно.
  3. Класс AdviceFolder создается с помощью переменной 'environment', указывающей на только что созданный объект EnvironmentParser, который, таким образом, должен иметь environment.A == 'herp' и environment.B = 'derp'.
  4. Утверждение должно завершиться успешно.

Но, видимо, где-то что-то идет не так, и я не могу указать, где именно.

В любом случае, если я хочу иметь модульные тесты как для значений по умолчанию для getenv, так и для значений, заданных вручную, как я могу выполнить их одновременно? Я мог бы запустить тест еще раз с внешними переменными окружения, но тогда один из двух тестов всегда терпел бы неудачу.


Воспроизводимый пример:

Создайте два файла Python:

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()

На самом деле, я ошибался раньше. Это первый метод тестирования, который терпит неудачу, потому что по какой-то причине значения A = «herp» и B = «derp» уже установлены даже в первом методе тестирования.

Тем не менее, существует проблема: я не могу одновременно проверять значения по умолчанию и не по умолчанию. Думаю, можно del из os.environ, но наверняка есть способ получше?

🤔 А знаете ли вы, что...
В Python есть множество библиотек и фреймворков для разработки веб-приложений.


2
92
1

Ответ:

Решено

Здесь происходит то, что модульные тесты выполняются в лексикографическом порядке. Это означает, что хотя test_example_with_custom_values() определен после test_example_with_default_values(), он запускается до него и устанавливаются переменные среды.

Одним из способов справиться с этим было бы использование подходов, предложенных в ссылке выше, например. переименуйте методы test_1() и test_2() или измените функцию unittest.TestLoader.sortTestMethodsUsing на функцию, которая будет сортировать значимые имена в желаемом порядке.

Однако в этом случае я думаю, что предпочтительнее не зависеть от порядка и вместо этого не оставлять переменные среды установленными после метода, который их изменяет, используя декоратор unittest.mock.patch():

patch() действует как декоратор функции, декоратор класса или менеджер контекста. Внутри тела функции или оператора with цель добавляется в новый объект. Когда оператор function/with завершает работу, исправление отменяется.

Итак, ваши тесты станут:

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()

Теперь это должно работать без ошибок:

$ python test_example.py 
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Если хотите, вы также можете добавить декоратор @patch.dict(os.environ, {}, clear=True) к test_example_with_default_values(), чтобы гарантировать, что он запускается в контексте с очищенными всеми переменными среды, хотя это не обязательно.