Django REST Framework:TestCase 未返回正确的查询集
Django REST Framework: TestCase is not returning correct queryset
我使用 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()
returns 只有第一个 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'
}]
这里有几点观察:
- 我的测试用例中返回的查询集不包含我的第一个产品的评论,即使通过邮递员访问时我可以看到评论。
Comments
是一个不同的 django
模型,可通过此嵌套端点访问:url(r'^products/(?P<product_id>[0-9]+)/comments/$', views.CommentsList.as_view())
- 我使用
POST
和 API
身份验证令牌插入了最后两个产品以及我的第一个产品的评论(none 在后一个查询集中返回) .这是我应该以某种方式包含在我的测试用例中的信息吗?
编辑
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
我怀疑,(手边没有您的产品模型)您没有从产品 table 中获取所有元素,原因如下:
- 您手动创建了前 9 个元素,没有将它们注册给特定用户。
- 之后,您添加了身份验证方法 (
TokenAuthentication
) 并创建了一些具有访问令牌的用户。
- 因为您添加了身份验证方法,所以您可能已将
@permission_classes((IsAuthenticated,)) / permission_classes=(IsAuthenticated,)
添加到 product-list
视图。
这会限制任何未经身份验证的用户访问 product-list
。
unauthenticated-anonymous 用户将只能看到您数据库的匿名元素。
- 您添加了接下来的 2 个元素和一位注册用户的评论,该注册用户又将这些元素注册到 user-creator,因此如果没有经过身份验证的用户,您将无法访问它们。
要从 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
方法 returns Products.objects.all()
正如@cezar 指出的那样,针对真实数据测试视图很容易失败(例如,当您添加新元素时,self.assertEqual(len(response.json()), 11)
将失败)
您应该考虑模拟您的回答以创建一个孤立的环境。
我倾向于使用 factory_boy
and django-nose
的组合(pytest
也可以)。
我使用 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()
returns 只有第一个 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'
}]
这里有几点观察:
- 我的测试用例中返回的查询集不包含我的第一个产品的评论,即使通过邮递员访问时我可以看到评论。
Comments
是一个不同的django
模型,可通过此嵌套端点访问:url(r'^products/(?P<product_id>[0-9]+)/comments/$', views.CommentsList.as_view())
- 我使用
POST
和API
身份验证令牌插入了最后两个产品以及我的第一个产品的评论(none 在后一个查询集中返回) .这是我应该以某种方式包含在我的测试用例中的信息吗?
编辑
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
我怀疑,(手边没有您的产品模型)您没有从产品 table 中获取所有元素,原因如下:
- 您手动创建了前 9 个元素,没有将它们注册给特定用户。
- 之后,您添加了身份验证方法 (
TokenAuthentication
) 并创建了一些具有访问令牌的用户。 - 因为您添加了身份验证方法,所以您可能已将
@permission_classes((IsAuthenticated,)) / permission_classes=(IsAuthenticated,)
添加到product-list
视图。
这会限制任何未经身份验证的用户访问product-list
。
unauthenticated-anonymous 用户将只能看到您数据库的匿名元素。 - 您添加了接下来的 2 个元素和一位注册用户的评论,该注册用户又将这些元素注册到 user-creator,因此如果没有经过身份验证的用户,您将无法访问它们。
要从 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
方法 returns Products.objects.all()
正如@cezar 指出的那样,针对真实数据测试视图很容易失败(例如,当您添加新元素时,self.assertEqual(len(response.json()), 11)
将失败)
您应该考虑模拟您的回答以创建一个孤立的环境。
我倾向于使用 factory_boy
and django-nose
的组合(pytest
也可以)。