如何在 django-rest-framework 中对权限进行单元测试?

Posted

技术标签:

【中文标题】如何在 django-rest-framework 中对权限进行单元测试?【英文标题】:How to unit test permissions in django-rest-framework? 【发布时间】:2016-09-07 08:14:14 【问题描述】:

她的权限是我想要进行单元测试的示例权限。

# permissions.py

from myapp.models import Membership

class IsOrganizationOwner(permissions.BasePermission):
    """
    Custom permission to allow only owner of the organization to do a certian task.
    """
    def has_object_permission(self, request, view, obj):
        try:
            membership = Membership.objects.get(user = request.user, organization = obj)
        except Membership.DoesNotExist:
            return False

        return membership.is_admin

这是如何应用的

# viewsets.py
class OrganizationViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows Organizations to be viewed or edited.
    """
    permission_classes = (permissions.IsAuthenticated, IsOrganizationOwner,)
    queryset = Organization.objects.all().order_by('name')
    serializer_class = OrganizationSerializer

现在我对在 django 中进行测试非常陌生,我不知道如何测试此权限。任何帮助将不胜感激。

【问题讨论】:

【参考方案1】:

这是一种方法:

from django_mock_queries.query import MockSet
from mock import patch, MagicMock
from unittest import TestCase


class TestPermissions(TestCase):
    memberships = MockSet()
    patch_memberships = patch('myapp.models.Membership.objects', memberships)

    def setUp(self):
        self.permission = IsOrganizationOwner()

        self.memberships.clear()
        self.request = MagicMock(user=MagicMock())
        self.view = MagicMock()

    def create_membership(self, organization, is_admin):
        self.request.user.is_admin = is_admin
        self.memberships.add(
            MagicMock(user=self.request.user, organization=organization)
        )

    @patch_memberships
    def test_permissions_is_organization_owner_returns_false_when_membership_does_not_exist(self):
        org = MagicMock()
        self.assertFalse(self.permission.has_object_permission(self.request, self.view, org))

    @patch_memberships
    def test_permissions_is_organization_owner_returns_false_when_membership_is_not_admin(self):
        org = MagicMock()
        self.create_membership(org, False)
        self.assertFalse(self.permission.has_object_permission(self.request, self.view, org))

    @patch_memberships
    def test_permissions_is_organization_owner_returns_true_when_membership_is_admin(self):
        org = MagicMock()
        self.create_membership(org, True)
        self.assertTrue(self.permission.has_object_permission(self.request, self.view, org))

我使用了我编写的 library 来模拟 django 查询集函数,以使测试更小且更具可读性。但如果您愿意,可以使用MockMagicMock 只修补您需要的东西。

编辑:为了完整起见,假设您还想进行集成测试OrganizationViewSet,以下是一些测试:

from django.contrib.auth.models import User
from django.test import TestCase, Client
from model_mommy import mommy


class TestOrganizationViewSet(TestCase):
    url = '/organizations/'

    def create_user(self, is_admin):
        password = 'password'

        user = mommy.prepare(User, is_admin=is_admin)
        user.set_password(password)
        user.save()

        return user, password

    def get_organizations_as(self, user=None, password=None):
        api = Client()

        if user:
            mommy.make(Membership, user=user, organization=mommy.make(Organization))
            api.login(username=user.username, password=password)

        return api.get(self.url)

    def test_organizations_viewset_returns_200_for_admins(self):
        response = self.get_organizations_as(*self.create_user(True))
        self.assertEqual(response.status_code, 200)

    def test_organizations_viewset_returns_403_for_non_admins(self):
        response = self.get_organizations_as(*self.create_user(False))
        self.assertEqual(response.status_code, 403)

    def test_organizations_viewset_returns_403_for_anonymous(self):
        response = self.get_organizations_as()
        self.assertEqual(response.status_code, 403)

正如其他人所指出的,您也需要这些测试,只是不是针对所有可能的测试用例。单元测试是最好的,因为集成测试会写入数据库等,并且会使你的 CI 过程变慢——以防这类事情与你有关。

【讨论】:

这看起来是一个非常不错的方法。如果这能解决我的问题,我会告诉你的。 这确实测试了权限本身是否有效;通过使用 django 的测试客户端向端点发送请求以确保按预期返回 403 来测试权限是否正确应用于视图也很有用。 是的,但 OP 要求提供unit test permissions 的方法,而不是集成测试OrganizationViewSet 的方法。尽管如此,我还是更新了我的答案以包括对此的测试。 谢谢,它帮助了我。但是,我想改进某个地方,即在 MagicMock 上使用 MagicMock 方法创建成员资格是因为无法使用 Model.objects.filter 作为方面。相反,需要使用django_mock_queries.MockModel 类。【参考方案2】:

我自己也在为此争论不休,我想我找到了一个简单的解决方案,可以单独测试权限检查的行为,而无需模拟所有内容。由于这个响应是在最初的答案之后 4 年才出现的,因此 Django 从那时起可能已经有了很大的发展。

测试权限似乎就像实例化权限并使用人为对象测试其has_permission 方法一样简单。比如我用IsAdminUser权限测试了这个,测试通过了:

from django.contrib.auth.models import User
from django.test import RequestFactory, TestCase
from rest_framework.permissions import IsAdminUser


class IsAdminUserTest(TestCase):
    def test_admin_user_returns_true(self):
        admin_user = User.objects.create(username='foo', is_staff=True)
        factory = RequestFactory()

        request = factory.delete('/')
        request.user = admin_user

        permission_check = IsAdminUser()

        permission = permission_check.has_permission(request, None)

        self.assertTrue(permission)

在 User 实例化中将 is_staff 更改为 False 会导致测试失败,如我所料。

用一个实际的例子来更新这个

我编写了自己的自定义权限来检查用户是否是管理员(员工用户),否则只允许只读操作。请注意,由于这是一个单元测试,它不与任何端点交互,甚至不寻求模拟它们;它只是测试权限检查的预期行为。

这里是许可:

from rest_framework import permissions


class IsAdminUserOrReadOnly(permissions.BasePermission):
    def has_permission(self, request, view):
        if request.method in permissions.SAFE_METHODS:
            return True

        return request.user.is_staff

这是完整的单元测试套件:

from django.contrib.auth.models import User
from django.test import RequestFactory, TestCase
from community.permissions import IsAdminUserOrReadOnly


class IsAdminOrReadOnlyTest(TestCase):
    def setUp(self):
        self.admin_user = User.objects.create(username='foo', is_staff=True)
        self.non_admin_user = User.objects.create(username='bar')
        self.factory = RequestFactory()

    def test_admin_user_returns_true(self):
        request = self.factory.delete('/')
        request.user = self.admin_user

        permission_check = IsAdminUserOrReadOnly()

        permission = permission_check.has_permission(request, None)

        self.assertTrue(permission)

    def test_admin_user_returns_true_on_safe_method(self):
        request = self.factory.get('/')
        request.user = self.admin_user

        permission_check = IsAdminUserOrReadOnly()

        permission = permission_check.has_permission(request, None)

        self.assertTrue(permission)

    def test_non_admin_user_returns_false(self):
        request = self.factory.delete('/')
        request.user = self.non_admin_user

        permission_check = IsAdminUserOrReadOnly()

        permission = permission_check.has_permission(request, None)

        self.assertFalse(permission)

    def test_non_admin_user_returns_true_on_safe_method(self):
        request = self.factory.get('/')
        request.user = self.non_admin_user

        permission_check = IsAdminUserOrReadOnly()

        permission = permission_check.has_permission(request, None)

        self.assertTrue(permission)

我假设您可以对几乎任何想要写入权限的用户属性使用类似的模式。

当我进行集成/功能测试时,我才会担心此权限如何影响 API 的接口。

【讨论】:

【参考方案3】:

作为上述方法的一个小增强/替代方案,我将使用 pytest 为 django 支持提供的 admin_user 固定装置:

https://pytest-django.readthedocs.io/en/latest/helpers.html#admin-user-an-admin-user-superuser

因此,快乐路径测试可能如下所示:

    def test_returns_200_when_user_is_authenticated(self, 
         client, admin_user):
    client.force_login(admin_user)

    response = client.put('/my_url/pk/', data='some data',
        content_type='application/json'))

    assert response.status_code == 200

您还可以测试用户未登录的相反场景:

   def test_returns_403_when_user_is_not_authenticated(
        self, client):

    response = client.put('/my_url/pk/', data='some data',
        content_type='application/json'))

    assert response.status_code == 403

【讨论】:

以上是关于如何在 django-rest-framework 中对权限进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 TemplateHTMLRenderer 在 Django-REST-Framework 中创建/放置?

如何使 DRF ( Django-REST-Framework) 令牌保持不变,使其在每次页面刷新后不会丢失?

django-rest-framework:如何序列化已经包含 JSON 的字段?

如何在 React 中显示来自 django-rest-framework 的错误消息

如何使用 django-rest-framework 进行社交登录? [关闭]

django-rest-framework:如何更新嵌套的外键?我的更新方法甚至没有被调用