Django在单元测试中设置环境变量

45
我希望能够在我的 Django 应用程序中设置环境变量,以便能够运行测试。例如,我的视图依赖于多个 API 密钥。
有一些方法可以在测试期间覆盖设置,但我不想将它们定义在settings.py中,因为这是一个安全问题。
我已经尝试在我的设置函数中设置这些环境变量,但这不能给 Django 应用程序提供值。
class MyTests(TestCase):
    def setUp(self):
        os.environ['TEST'] = '123'  # doesn't propogate to app

当我本地测试时,我只需运行一个名为.env的文件。
foreman start -e .env web

这个函数会为os.environ提供值。但是在Django的unittest.TestCase中,我不知道有没有设置的方法。

我该如何解决这个问题?


2
你尝试过使用EnvironmentVarGuard吗? - schillingt
是的,那就是正确的答案! - lollercoaster
10个回答

50

test.support.EnvironmentVarGuard 是一个内部 API,可能会在版本之间进行更改并带有破坏性(不兼容)的更改。 实际上,整个test包仅供内部使用。 在 test 包文档页面上明确说明了它是用于核心库的内部测试,而不是公共 API。(参见下面的链接)

您应该在 Python 标准库中使用unittest.mock中的patch.dict()。它可以作为上下文管理器、装饰器或类装饰器使用。请参见下面从官方 Python 文档中复制的示例代码。

import os
from unittest.mock import patch
with patch.dict('os.environ', {'newkey': 'newvalue'}):
    print(os.environ['newkey'])  # should print out 'newvalue'
    assert 'newkey' in os.environ  # should be True
assert 'newkey' not in os.environ  # should be True

更新:对于那些没有认真阅读文档可能错过了注释的人,可以在以下链接中阅读更多test包的说明:

https://docs.python.org/2/library/test.html 或者

https://docs.python.org/3/library/test.html


5
在你提供的链接中,它是锚定在 class test.test_support.EnvironmentVarGuard 上的,换句话说,它是 test 包的一部分,这是 Python 的回归测试包。然后将页面滚动到最顶部,阅读标题右侧第二行的注释:注意 test 包仅供 Python 内部使用。它是为了 Python 核心开发人员的利益而记录的。不建议在 Python 标准库之外使用此软件包,因为此处提到的代码可能会在 Python 发行版本之间更改或删除而没有任何通知。 - Devy

17

如果您正在像这样在Django的settings.py文件中加载环境变量:

import os
ENV_NAME = os.environ.get('ENV_NAME', 'default')
你可以使用这个:
from django.test import TestCase, override_settings

@override_settings(ENV_NAME="super_setting")
def test_...(self):

2
这是一个非常好的答案,因为将 os.environ 变量引入到 settings.py 中并且在整个代码中使用 这些 变量(比如 from django.conf import settings)是一种常见的做法。如果您的代码使用了 my_obj = MyClass(arg=settings.MY_ENV_VAR) 这样的语句,那么您需要使用 @override_settings() 包装器来正确地设置您的 MY_ENV_VAR 单元测试。如果您以其他方式设置了 os.environ 变量,my_obj 将无法正确实例化。 - s g
1
使用Django自带的override_settings是一个很好的答案。 - Hammad

12

正如评论中 @schillingt 所指出的那样,EnvironmentVarGuard 是正确的方法。

from test.test_support import EnvironmentVarGuard # Python(2.7 < 3)
from test.support import EnvironmentVarGuard # Python >=3
from django.test import TestCase

class MyTestCase(TestCase):
    def setUp(self):
        self.env = EnvironmentVarGuard()
        self.env.set('VAR', 'value')

    def test_something(self):
        with self.env:
            # ... perform tests here ... #
            pass

使用with语句正确设置环境变量,生效范围为上下文对象的持续时间。


3
引发导入错误。另外,EnvironmentVarGuard的文档声明:“警告:测试包仅供Python内部使用。此处提供文档是为了使Python核心开发人员受益。不建议在Python标准库之外使用此软件包,因为所述代码可能会在Python版本更新时改变或被删除,而没有任何通知。” - Nate
3
Python 3 把 EnvironmentVarGuard 导入改为了 from test.support import EnvironmentVarGuard。但是,如果你不想依赖于内部使用的代码,你可以将 Python 2.7 中 EnvironmentVarGuard 的实现 复制到你自己的代码中 -- 这很简单。 - medmunds

7

EnvironmentVarGuard 不是一个好的解决方案,因为它在某些环境中失败,在其他环境中工作。请看下面的示例。

Python3.6 environment on gitlab ci

更好的解决方案是由 erewok 建议的,需要利用 Python3 中的 unittest.mock

假设使用 unittest。

from unittest.mock import patch
class TestCase(unittest.TestCase):

    def setUp(self):
        self.env = patch.dict('os.environ', {'hello':'world'})

    def test_scenario_1(self):
        with self.env:
            self.assertEqual(os.environ.get('hello'), 'world')

```


1
根据文档 https://docs.python.org/3/library/unittest.mock.html?highlight=mock#patch-methods-start-and-stop : "如果您使用此技术,必须确保通过调用stop来“撤消”修补。" 此示例未对环境进行“取消修补”,因此可能会破坏其他测试。 - jamesc

1
我使用 py.test 作为我的测试运行器,它允许你创建一个 pytest.ini 文件,在其中指定运行测试时要使用的特定设置文件。
在此处查看相关文档:

http://pytest-django.readthedocs.org/en/latest/configuring_django.html#pytest-ini-settings

我一般建议使用py.test作为测试运行器,因为它支持不同类型的测试类和简单函数,并且很容易设置在测试前后运行的fixtures或其他代码。


1
这是如何更改 settings.py 文件,而不是环境变量。例如,我的 API 调用到 AWS 等服务时,使用的是构造函数来查找环境变量,而不是 Django 的设置。 - lollercoaster
你的测试将使用真实的 API 密钥和凭据吗?它们实际上会与第三方 API 进行通信? - erewok
我从未考虑过测试第三方API。 - erewok
不,你不是在测试第三方API。你正在测试你对这些第三方API的服务配置以及你设置的API接口是否与所有边缘情况一样按预期工作。这不仅是完全可接受的做法,不这样做简直是疯了。 - lollercoaster
除了配置之外,我们通常会使用模拟接口来测试与 API 通信的应用程序代码。我想我不希望我的 CI 服务器调用 API。我也不希望它意外地发送电子邮件或以其他方式与世界交流。 - erewok
显示剩余3条评论

1
本文解释了如何在Python的单元测试中模拟环境变量,以下代码片段对我很有用: 如何在Python的unittest中模拟环境变量
      import os
    from unittest import TestCase, mock

class SettingsTests(TestCase):
    @classmethod
    def setUpClass(cls):
        cls.env_patcher = mock.patch.dict(os.environ, {"FROBNICATION_COLOUR": "ROUGE"})
        cls.env_patcher.start()

        super().setUpClass()

    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()

        cls.env_patcher.stop()

    def setUp(self):
        super().setUp()
        self.assertEqual(os.environ["FROBNICATION_COLOUR"], "ROUGE")

    def test_frobnication_colour(self):
        self.assertEqual(os.environ["FROBNICATION_COLOUR"], "ROUGE")

0

虽然这是一个老问题,但它在谷歌搜索中出现了,而且现有的答案都不合适。如果你正在使用pytest,可以使用pytest的monkeypatching功能来设置/恢复环境变量。


0

0
Django 4.x版本中,你可以在测试时使用mock.patch.dict来覆盖os.environ,例如:
import os
from unittest import mock

from django.test import TestCase


class ExampleTestCase(TestCase):

    @mock.patch.dict(os.environ, {
        "CREDENTIAL_PATH": "/example/fixtures/tests/test-credential.json"
    })
    def test_initialize_firebase_credential_if_needed(self):
        print(f"CREDENTIAL_PATH: {os.environ.get('CREDENTIAL_PATH')}")

-1

最初,我的环境变量PARTNER_CODE被设置为wow

我可以使用以下方法更改环境变量:

from test.support import EnvironmentVarGuard
with EnvironmentVarGuard() as env:
   env['PARTNER_CODE'] = 'sos'

现在我的环境变量PARTNER_CODE显示为sos


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接