【问题标题】:Django REST Framework: TestCase is not returning correct querysetDjango REST Framework:TestCase 没有返回正确的查询集
【发布时间】:2018-09-10 12:05:04
【问题描述】:

我使用DRF为库存中的产品创建了一个API,可以通过以下端点url(r'products/$', views.InventoryList.as_view(), name='product-list')访问。

当通过邮递员发出GET 请求时,我得到了正确的查询集,总共有11 产品:

[
    {
        "id": 1,
        "name": "Biscuits",
        "description": "Papadopoulou Biscuits",
        "price": "2.52",
        "comments": [
            {
                "id": 1,
                "title": "First comments for this",
                "comments": "Very tasty",
                "rating": 8,
                "created_by": "xx"
            }
        ]
    },
    {
        "id": 2,
        "name": "Rice",
        "description": "Agrino Rice",
        "price": "3.45",
        "comments": []
    },
    {
        "id": 3,
        "name": "Spaghetti",
        "description": "Barilla",
        "price": "2.10",
        "comments": []
    },
    {
        "id": 4,
        "name": "Canned Tomatoes",
        "description": "Kyknos",
        "price": "3.40",
        "comments": []
    },
    {
        "id": 5,
        "name": "Bacon",
        "description": "Nikas Bacon",
        "price": "2.85",
        "comments": []
    },
    {
        "id": 6,
        "name": "Croissants",
        "description": "Molto",
        "price": "3.50",
        "comments": []
    },
    {
        "id": 7,
        "name": "Beef",
        "description": "Ground",
        "price": "12.50",
        "comments": []
    },
    {
        "id": 8,
        "name": "Flour",
        "description": "Traditional Flour",
        "price": "3.50",
        "comments": []
    },
    {
        "id": 9,
        "name": "Oregano",
        "description": "Traditional oregano",
        "price": "0.70",
        "comments": []
    },
    {
        "id": 10,
        "name": "Tortellini",
        "description": "Authentic tortellini",
        "price": "4.22",
        "comments": []
    },
    {
        "id": 11,
        "name": "Milk",
        "description": "Delta",
        "price": "1.10",
        "comments": []
    }
]

然后我写了一个测试(使用 pytest )来测试这个端点:

import pytest
import pytest_django
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase

class TestInventoryList(APITestCase):
    @pytest.mark.django_db
    def test_get_product_list(self):
        url = reverse('product-list')
        response = self.client.get(url)
        print(response.json())
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.json()), 11) # <-- TC fails here

但它失败了,因为 response.json() 只返回前 9 个对象:

[{
    'id': 1,
    'name': 'Biscuits',
    'description': 'Papadopoulou Biscuits',
    'comments': [],
    'price': '2.52'
}, {
    'id': 2,
    'name': 'Rice',
    'description': 'Agrino Rice',
    'comments': [],
    'price': '3.45'
}, {
    'id': 3,
    'name': 'Spaghetti',
    'description': 'Barilla',
    'comments': [],
    'price': '2.10'
}, {
    'id': 4,
    'name': 'Canned Tomatoes',
    'description': 'Kyknos',
    'comments': [],
    'price': '3.40'
}, {
    'id': 5,
    'name': 'Bacon',
    'description': 'Nikas Bacon',
    'comments': [],
    'price': '2.85'
}, {
    'id': 6,
    'name': 'Croissants',
    'description': 'Molto',
    'comments': [],
    'price': '3.50'
}, {
    'id': 7,
    'name': 'Beef',
    'description': 'Ground',
    'comments': [],
    'price': '12.50'
}, {
    'id': 8,
    'name': 'Flour',
    'description': 'Traditional Flour',
    'comments': [],
    'price': '3.50'
}, {
    'id': 9,
    'name': 'Oregano',
    'description': 'Traditional oregano',
    'comments': [],
    'price': '0.70'
}]

这里有几个观察:

  1. 在我的测试用例中返回的查询集不包含我的第一个产品的 cmets,尽管通过邮递员访问时我可以看到 cmets。 Comments 是一个不同的 django 模型,可通过此嵌套端点访问:url(r'^products/(?P&lt;product_id&gt;[0-9]+)/comments/$', views.CommentsList.as_view())
  2. 我使用POSTAPI 身份验证令牌插入了最后两个产品以及我的第一个产品的评论(后一个查询集中没有返回)。这是我应该以某种方式包含在我的测试用例中的信息吗?

编辑

models.py

    from django.db import models
from django.contrib.auth.models import User

class Product(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField()
    price = models.DecimalField(decimal_places=2, max_digits=20)


class Comments(models.Model):
    product = models.ForeignKey(Product, related_name='comments')
    title = models.CharField(max_length=255)
    comments = models.TextField()
    rating = models.IntegerField()
    created_by = models.ForeignKey(User)

urls.py

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'products/$', views.InventoryList.as_view(), name='product-list'),
    url(r'^products/(?P<product_id>[0-9]+)/$', views.InventoryDetail.as_view()),
    url(r'^products/(?P<product_id>[0-9]+)/comments/$', views.CommentsList.as_view()),
    url(r'^products/(?P<product_id>[0-9]+)/comments/(?P<comment_id>[0-9]+)/$', views.CommentsDetail.as_view()),
]

views.py

from rest_framework import generics
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Product, Comments
from .serializers import ProductSerializer, CommentSerializer
from .permissions import IsAdminOrReadOnly, IsOwnerOrReadOnly


class InventoryList(generics.ListCreateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    permission_classes = (IsAdminOrReadOnly, )
    lookup_url_kwarg = 'product_id'


class InventoryDetail(generics.RetrieveUpdateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    permission_classes = (IsAdminOrReadOnly, )
    lookup_url_kwarg = 'product_id'


class CommentsList(generics.ListCreateAPIView):
    serializer_class = CommentSerializer
    permission_classes = (IsAuthenticatedOrReadOnly, )
    lookup_url_kwarg = 'product_id'

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user, product_id=self.kwargs['product_id'])

    def get_queryset(self):
        product = self.kwargs['product_id']
        return Comments.objects.filter(product__id=product)


class CommentsDetail(generics.RetrieveUpdateDestroyAPIView):
    serializer_class = CommentSerializer
    permission_classes = (IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)
    lookup_url_kwarg = 'comment_id'

    def get_queryset(self):
        comment = self.kwargs['comment_id']
        return Comments.objects.filter(id=comment)

permissions.py

from rest_framework.permissions import BasePermission, SAFE_METHODS


class IsAdminOrReadOnly(BasePermission):
    def has_permission(self, request, view):
        if request.method in SAFE_METHODS:
            return True
        else:
            return request.user.is_staff


class IsOwnerOrReadOnly(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True

        return obj.created_by == request.user

【问题讨论】:

  • 想到的事情:不同的数据库或用于测试的不同分页设置。
  • 您有什么具体的检查可以让我验证您的两个建议吗?据我记得,我没有改变任何关于分页的东西,我只在我的虚拟环境中使用一个数据库。您认为问题可能与 API 令牌有关吗?
  • 至于db,看表中是否有实体:测试加assert Product.objects.get(pk=10).name == 'Tortellini',测试是否通过?
  • 至于分页,再想想这可能不是问题,因为您也错过了第一个产品的 cmets。
  • 为什么不使用setUpClasssetUpTestData 之类的东西来测试您的视图?它将更加健壮并且独立于您的生产数据。

标签: python django django-rest-framework pytest django-testing


【解决方案1】:

我怀疑(手头没有您的产品模型)您没有从 products 表中获取所有元素,原因如下:

  • 您手动创建了前 9 个元素,而没有将它们注册给特定用户。
  • 之后,您添加了一种身份验证方法 (TokenAuthentication) 并创建了一些具有访问令牌的用户。
  • 由于您添加了身份验证方法,您可能将@permission_classes((IsAuthenticated,)) / permission_classes=(IsAuthenticated,) 添加到您的product-list 视图中。
    这会限制任何未经身份验证的用户访问product-list
    未经身份验证的匿名用户将只能查看数据库中的匿名元素。
  • 您添加了接下来的 2 个元素和评论,其中一位注册用户又将这些元素注册到了用户创建者,因此如果没有经过身份验证的用户,您将无法访问它们。

要从 DRF 的测试客户端访问需要身份验证的资源,您需要先对用户进行身份验证。
你可以使用force_authenticate方法:

class TestInventoryList(APITestCase):
    def setUp(self):
        self.req_factory = APIRequestFactory()
        self.view =  views.InventoryList.as_view({'get': 'list',})

    @pytest.mark.django_db
    def test_get_product_list(self):
        url = reverse('product-list')
        request = self.client.get(url)
        force_authenticate(request, user=YOUR_USER)
        response = self.view(request)
        print(response.json())
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.json()), 11)

这个测试假设你list方法返回Products.objects.all()


正如@cezar 指出的那样,针对真实数据测试视图很容易失败(例如,当您添加新元素时,self.assertEqual(len(response.json()), 11) 将失败)

您应该考虑模拟您的响应以创建一个孤立的环境。

我倾向于结合使用factory_boydjango-nosepytest 也可以)。

【讨论】:

  • 嗨。感谢您的回复。首先,我编辑了我的问题以添加model。其次,你的逻辑是对的。我只希望admin 用户添加产品,所以我只允许该类型的用户使用POST 方法。然后我创建了一个端点(请参阅urls.py),它会在传递用户名和密码后将token 返回给客户端。然后我创建了一个django superuser 并传递了这个用户名和密码来获取token。然后我创建了适当的permission 类(请参阅相应的文件)并使用我的令牌发出POST 请求。
  • 关于您的代码;尽管在将我指向正确的方向(即force_authentication 函数)方面很有帮助,但它对我不起作用。具体来说,它表示self.view 行中存在参数错误。除了我的代码之外,由于某种原因,找不到我在上面创建的用户。因此force_authenticate(request, user=YOUR_USER) 行也失败了。这很奇怪,因为我可以在 auth_user 表中看到用户,但是当我执行 User.object.all() 查询时,我无法检索它。
猜你喜欢
  • 2020-08-28
  • 2019-12-13
  • 2016-05-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-05
相关资源
最近更新 更多