У меня есть класс 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
, поэтому порядок выполнения должен быть
os.environ['A']
и os.environ['B']
установлены на 'herp'
и 'derp'
соответственно;'A'
и 'B'
у os.environ
, следовательно, эти значения должны быть 'herp'
и 'derp'
соответственно.'environment'
, указывающей на только что созданный объект EnvironmentParser, который, таким образом, должен иметь environment.A == 'herp'
и environment.B = 'derp'
.Но, видимо, где-то что-то идет не так, и я не могу указать, где именно.
В любом случае, если я хочу иметь модульные тесты как для значений по умолчанию для 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 Это справедливо. Кажется, это связано с юниттестом. Скопировав код из unittest в раздел if name == "main" в файле, где определен класс AdviceFolder, я не могу воспроизвести это. Посмотрим, смогу ли я создать минимальный воспроизводимый пример, включая модульный тест.
Нет необходимости помещать это в два отдельных файла, или? Я могу запустить два теста по отдельности, и они оба пройдут успешно, только когда я прогоню весь костюм TestExample
, тест значения по умолчанию завершится неудачей. Эта часть не совсем ясна в вашем вопросе.
Если вы не планируете создавать другие программы, использующие переменные среды, я бы не стал модифицировать среду и разрабатывать Config
так, чтобы она принимала явные аргументы, а не только читала из среды.
Здесь происходит то, что модульные тесты выполняются в лексикографическом порядке. Это означает, что хотя 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()
, чтобы гарантировать, что он запускается в контексте с очищенными всеми переменными среды, хотя это не обязательно.
Этот вопрос выиграет от минимально воспроизводимого примера — кода, который мы можем запустить локально, чтобы воспроизвести проблему. Например, мы не знаем, что происходит в
AdviceFolder
; возможно, ошибка кодирования усугубляет проблему.