在Flask单元测试中重写flask.g

7 投票
2 回答
4033 浏览
提问于 2025-04-18 03:21

我正在为我的Flask应用写一些单元测试,这个应用使用OpenID进行身份验证。因为似乎没有办法通过OpenID让测试客户端登录(我问过这个问题,但还没有人回答:Flask OpenID unittest),所以我想在测试中覆盖g.user的值,于是我试了试http://flask.pocoo.org/docs/testing/#faking-resources-and-context上的代码片段,结果如我所愿地工作了。

不幸的是,当使用flask-login时,g.user在before_request这个包装器中被覆盖,这个包装器会设置

g.user = current_user

current_user为匿名用户,所以我的测试用例就坏掉了。一个解决办法是只在测试模式下执行before_request的代码,但在生产代码中添加特定于测试的逻辑感觉很糟糕。我也尝试过搞定请求上下文,但g.user最终还是会被覆盖。有没有什么干净的办法来解决这个问题?

2 个回答

-1

根据另一个问题在Flask单元测试中,如何模拟请求全局的 `g` 对象上的对象?,我找到了一种方法:

在我的应用中,我把登录逻辑从 `before_request` 中提取出来,放到了一个单独的函数里。然后,在我的测试中,我对这个函数进行了“补丁”,让它返回我想用的特定用户,这样我就可以在多个测试中使用。虽然 `before_request` 仍然会在测试中运行,但通过补丁这个函数,我就可以避免实际的登录过程。

我不确定这是不是最干净的做法,但我觉得比在 `before_request` 中添加仅用于测试的逻辑要好,这只是一个重构。

1

官方文档中有一个关于“伪造资源和上下文”的例子,具体可以查看这里

首先,你需要确保只有在g.user还不存在的情况下才设置它。你可以使用getattr来做到这一点。下面是一个稍微修改过的例子:

@APP.before_request
def set_user():
    user = getattr(g, 'user', None)
    if user is None:
        g.user = concrete_implementation()
    return user

通过使用getattr,我们可以在测试时“注入”一些东西。如果不这样做,单元测试注入的值会被具体的实现覆盖掉。

接下来,我们要做的是监听appcontext_pushed事件,并用一个可以测试的值来设置g.user的值。这是在before_request钩子之前发生的。所以在调用before_request时,getattr会返回我们的测试值,而跳过具体的实现:

from contextlib import contextmanager
from flask import appcontext_pushed, g

@contextmanager
def user_set(app, user):
    def handler(sender, **kwargs):
        g.user = user
    with appcontext_pushed.connected_to(handler, app):
        yield

有了这个小工具,我们可以在需要使用可测试的值时随时注入一些东西:

def test_user_me():
    with user_set(app, 'our-testable-user'):
        c = app.test_client()
        resp = c.get('/protected-resource')
        assert resp.data == '...'

撰写回答