在App Engine中如何模拟用户服务?

22

我正在使用Google App Engine的testbed框架编写带有模拟对象的测试用例,文档可以在这里找到。我已经通过使用模拟数据库(Testbed.init_datastore_v3_stub)成功地进行了数据存储测试,这使得我的测试用例可以在一个快速而新鲜的数据库上运行,每个测试用例都可以重新初始化。现在我想要测试依赖于当前用户的功能。

还有另一个名为Testbed.init_user_stub的testbed服务,我可以激活这个服务来获取“虚假”的用户服务。不幸的是,似乎没有任何关于它的文档。我正在像这样激活和使用它:

import unittest
from google.appengine.ext import testbed
from google.appengine.api import users

class MyTest(unittest.TestCase):
    def setUp(self):
        self.testbed = testbed.Testbed()
        self.testbed.activate()
        self.testbed.init_user_stub()

    def testUser(self):
        u = users.get_current_user()
        self.assertNotEqual(u, None)
问题在于我无法找到任何方法告诉“假”的用户服务去认证一个“假”的用户。因此,在运行测试时,我(可以预料地)得到了
AssertionError: None == None
意思是虚假用户服务告诉我的应用程序当前用户未登录。我该如何告诉虚假用户服务假装某个用户已登录?理想情况下,我希望能够指定虚假用户的昵称、电子邮件、用户ID以及他们是否是管理员。这似乎是一个非常普遍的需求(因为需要测试应用程序在a) 没有用户登录,b) 用户已登录和c) 管理员已登录的情况下的行为方式),但搜索“init_user_stub”几乎什么都没有返回。
注意:如果您要测试上面的程序,则需要将此添加到顶部:
import sys
sys.path.append('/PATH/TO/APPENGINE/SDK')
import dev_appserver
dev_appserver.fix_sys_path()

并将此添加到底部:

if __name__ == '__main__':
    unittest.main()
4个回答

17

我认为没有正式的方法来做到这一点,但我已经阅读了源代码,并找到了一种“hack”方式来做到这一点,迄今为止运行良好。(通常我会担心使用未记录的行为,但这只是一个测试套件,所以它只有在开发服务器上工作才有意义。)

开发服务器根据三个环境变量确定当前登录用户:

  • USER_EMAIL: 用户的电子邮件地址,以及用户的昵称。
  • USER_ID: 用户的唯一 Google ID(字符串)。
  • USER_IS_ADMIN: 如果用户为非管理员,则为“0”,如果用户为管理员,则为“1”。

您可以使用 os.environ 设置它们,就像设置任何其他环境变量一样,它们会立即生效(显然,这在生产服务器上不起作用)。但是,您可以将其与 testbed 的 user_stub 一起使用,当您停用 testbed 时,它们将被重置(您应该在 tearDown 中执行此操作,以便每个测试用例都获得一个新的环境)。

由于设置环境变量有点笨拙,因此我编写了一些包装函数来打包它们:

import os

def setCurrentUser(email, user_id, is_admin=False):
    os.environ['USER_EMAIL'] = email or ''
    os.environ['USER_ID'] = user_id or ''
    os.environ['USER_IS_ADMIN'] = '1' if is_admin else '0'

def logoutCurrentUser():
    setCurrentUser(None, None)

11
基本上这是正确的想法,除了你希望使用testbed.setup_env()而不是直接使用os.environ - ryan
1
我要给这个点踩,因为这是在污染环境。需要注意的是,需要进行清理。可以像@ryan所说的那样使用测试平台来完成清理。 - siebz0r

11

以下是我用来模拟已登录用户的方法:

self.testbed.setup_env(USER_EMAIL='usermail@gmail.com',USER_ID='1', USER_IS_ADMIN='0')
self.testbed.init_user_stub()

可以运行,但是似乎testbed.setup_env()必须在testbed.activate()之前才会生效。 - garst
@gargc 很抱歉要说你所说的是错误和可能的“危险”。请看我的答案进行演示。 - siebz0r
1
@siebz0r 好主意!顺便说一下,我刚刚发现为什么我不得不改变调用的顺序才能使它工作。USER_EMAIL和USER_ID在激活时被设置为空字符串,作为默认环境的一部分(这在user_is_admin中不会发生)。解决方案是将overwrite=True传递给setup_env,否则环境中已经存在的任何变量都会被忽略,导致get_current_user()始终为None。 - garst
这需要我使用overwrite参数,如此答案所述。 - klenwell

10

除了Bijan的回答之外:

google.appengine.api.users中实际检查的代码如下:

def is_current_user_admin():
    return (os.environ.get('USER_IS_ADMIN', '0')) == '1'
因此,关键是将环境变量USER_IS_ADMIN设置为'1'。这可以通过多种方式完成,但请注意,您正在修改全局变量,因此可能会影响其他代码。关键是要进行适当的清理。
可以使用Mock库修补 os.environ,使用Testbed或发挥自己的创造力。我更喜欢使用Testbed,因为它暗示了此 hack 与 appengine 相关。Mock未包含在Python 3.3之前的版本中,因此这会增加额外的测试依赖项。
额外说明:在使用unittest模块时,我更喜欢使用addCleanup而不是tearDown,因为当setUp失败时也会调用清理。
示例测试:
import unittest

from google.appengine.api import users
from google.appengine.ext import testbed


class AdminTest(unittest.TestCase):
    def setUp(self):
        tb = testbed.Testbed()
        tb.activate()
        # ``setup_env`` takes care of the casing ;-)
        tb.setup_env(user_is_admin='1')
        self.addCleanup(tb.deactivate)

    def test_is_current_user_admin(self):
        self.assertTrue(users.is_current_user_admin())

注意:Testbed.setup_env应该在Testbed.activate之后调用。 Testbed在激活时会对os.environ进行快照,当取消激活时会恢复这个快照。如果在激活之前调用Testbed.setup_env,则会修改真正的os.environ而不是临时实例,从而有效地污染环境。

这个行为是正确的:

>>> import os
>>> from google.appengine.ext import testbed
>>> 
>>> tb = testbed.Testbed()
>>> tb.activate()
>>> tb.setup_env(user_is_admin='1')
>>> assert 'USER_IS_ADMIN' in os.environ
>>> tb.deactivate()
>>> assert 'USER_IS_ADMIN' not in os.environ
>>> 

这会污染环境:

>>> import os
>>> from google.appengine.ext import testbed
>>> 
>>> tb = testbed.Testbed()
>>> tb.setup_env(user_is_admin='1')
>>> tb.activate()
>>> assert 'USER_IS_ADMIN' in os.environ
>>> tb.deactivate()
>>> assert 'USER_IS_ADMIN' not in os.environ
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

0
这是我基于这里的答案编写用于测试的一些辅助函数。我把它们放在了一个test_helper模块中:
# tests/test_helper.py
import hashlib

def mock_user(testbed, user_email='test@example.com', is_admin=False):
    user_id = hashlib.md5(user_email).hexdigest()
    is_admin = str(int(is_admin))

    testbed.setup_env(USER_EMAIL=user_email,
                      USER_ID=user_id,
                      USER_IS_ADMIN=is_admin,
                      overwrite=True)
    testbed.init_user_stub()

def mock_admin_user(testbed, user_email='admin@example.com'):
    mock_user(testbed, user_email, True)

示例用法(使用NoseGAE):

import unittest

from google.appengine.ext import ndb, testbed
from google.appengine.api import users

from tests.test_helper import mock_user, mock_admin_user

class MockUserTest(unittest.TestCase):
    def setUp(self):
        self.testbed = testbed.Testbed()
        self.testbed.activate()
        self.testbed.init_datastore_v3_stub()
        self.testbed.init_memcache_stub()
        ndb.get_context().clear_cache()

    def tearDown(self):
        self.testbed.deactivate()

    def test_should_mock_user_login(self):
        self.assertIsNone(users.get_current_user())
        self.assertFalse(users.is_current_user_admin())

        mock_user(self.testbed)
        user = users.get_current_user()
        self.assertEqual(user.email(), 'test@example.com')
        self.assertFalse(users.is_current_user_admin())

        mock_admin_user(self.testbed)
        admin = users.get_current_user()
        self.assertEqual(admin.email(), 'admin@example.com')
        self.assertTrue(users.is_current_user_admin())

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