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, но наверняка есть способ получше?

Этот вопрос выиграет от минимально воспроизводимого примера — кода, который мы можем запустить локально, чтобы воспроизвести проблему. Например, мы не знаем, что происходит в AdviceFolder; возможно, ошибка кодирования усугубляет проблему.

larsks 04.09.2024 13:02

Вопрос к ОП; «Как и где создается экземпляр AdviceFolder?»

rasjani 04.09.2024 13:06

@larsks Это справедливо. Кажется, это связано с юниттестом. Скопировав код из unittest в раздел if name == "main" в файле, где определен класс AdviceFolder, я не могу воспроизвести это. Посмотрим, смогу ли я создать минимальный воспроизводимый пример, включая модульный тест.

Bence Racskó 04.09.2024 13:28

Нет необходимости помещать это в два отдельных файла, или? Я могу запустить два теста по отдельности, и они оба пройдут успешно, только когда я прогоню весь костюм TestExample, тест значения по умолчанию завершится неудачей. Эта часть не совсем ясна в вашем вопросе.

Ulrich Eckhardt 04.09.2024 21:29

Если вы не планируете создавать другие программы, использующие переменные среды, я бы не стал модифицировать среду и разрабатывать Config так, чтобы она принимала явные аргументы, а не только читала из среды.

chepner 04.09.2024 21:52
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
5
92
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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(), чтобы гарантировать, что он запускается в контексте с очищенными всеми переменными среды, хотя это не обязательно.

Другие вопросы по теме