如何将 Django 模拟实例传递给 class 方法?

How to pass Django mock instance to class method?

Mock 测试库是我似乎无法理解的 Django 主题之一。例如,在下面的代码中,为什么我在单元测试中创建的模拟 User 实例没有出现在我在 'get_user_ids' 方法中查询的 User 对象中?如果我通过调试调用在 'get_user_ids' 方法中停止测试并执行 "User.objects.all()",用户查询集中没有任何内容并且测试失败。我不是在创建三个将通过 UserProxy 的静态方法查询的模拟 User 实例吗?

我正在使用 Django 1.6 和 Postgres 9.3 并使用命令 "python manage.py test -s apps.profile.tests.model_tests:TestUserProxy".

运行测试

谢谢!

# apps/profile/models.py
from django.contrib.auth.models import User
class UserProxy(User):
    class Meta:
        proxy = True

    @staticmethod
    def get_user_ids(usernames):
        debug()
        user_ids = []
        for name in usernames:
            try:
                u = User.objects.get(username__exact=name)
                user_ids.append(u.id)
            except ObjectDoesNotExist:
                logger.error("We were unable to find '%s' in a list of usernames." % name)
        return user_ids


# apps/profile/tests/model_tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from mock import Mock
from apps.profile.models import UserProxy

class TestUserProxy(TestCase):
    def test_get_user_ids(self):
        u1 = Mock(spec=User)
        u1.id = 1
        u1.username = 'user1'

        u2 = Mock(spec=User)
        u2.id = 2
        u2.username = 'user2'

        u3 = Mock(spec=User)
        u3.id = 3
        u3.username = 'user3'

        usernames = [u1.username, u2.username, u3.username]
        expected = [u1.id, u2.id, u3.id]
        actual = UserProxy.get_user_ids(usernames)
        self.assertEqual(expected, actual)

Mocking 非常适合测试,并且可以导致非常干净的测试,但是它会受到一些影响(a)在开始时有点笨拙,并且(b)确实经常需要一些努力设置模拟对象,然后 injected/used 在正确的位置。

您为用户创建的模拟对象是看起来像 Django User 模型对象的对象,但它们不是实际的模型对象,因此不会被放入数据库。

要使测试正常运行,您有两种选择,具体取决于您要编写的测试类型。

单元测试-模拟从数据库返回的数据

第一个选项是将其作为单元测试运行,即独立于数据库层测试 get_user_ids 方法。为此,您需要模拟对 User.objects.get(username__exact=name) 的调用,以便它 returns 您在测试中创建的三个模拟对象。这将是更正确的方法(因为最好单独测试代码单元),但是与下面的替代方法相比,它需要更多的设置工作。

实现此目的的一种方法是首先将用户查找分离到它自己的函数中 apps/profile/models.py:

def get_users_by_name(name):
    return User.objects.get(username__exact=name)

这需要在您的函数中调用,方法是将对 Users.objects.get(username__exact=name) 的调用替换为 get_users_by_name(name)。然后,您可以像这样修改您的测试以修补函数:

from django.test import TestCase
from django.contrib.auth.models import User
from mock import Mock, patch
from apps.profile.models import UserProxy

class TestUserProxy(TestCase):

    @patch('apps.profile.models.get_user_by_name')
    def test_get_user_ids(self, mock_get_user_by_name):
        u1 = Mock(spec=User)
        u1.id = 1
        u1.username = 'user1'

        u2 = Mock(spec=User)
        u2.id = 2
        u2.username = 'user2'

        u3 = Mock(spec=User)
        u3.id = 3
        u3.username = 'user3'

        # Here is where we wire up the mocking - we take the patched method to return
        # users and tell it that, when it is called, it must return the three mock
        # users you just created.
        mock_get_user_by_name.return_value = [u1, u2, u3]

        usernames = [u1.username, u2.username, u3.username]
        expected = [u1.id, u2.id, u3.id]
        actual = UserProxy.get_user_ids(usernames)
        self.assertEqual(expected, actual)

集成测试 - 创建真实用户对象

第二种方法是将其修改为集成测试,即测试该代码单元以及与数据库的交互。这有点不太干净,因为您现在将对该方法的测试暴露给由于不同代码单元(即与数据库交互的 Django 代码)中的问题而失败的机会。但是,这确实使测试的设置变得更加简单,并且从实用的角度来说可能是适合您的方法。

为此,只需删除您创建的模拟并在数据库中创建实际用户作为测试的一部分。