Pytest-Django测试提交JSON数据失败致400断言错误求助
Django创建商品接口测试返回400错误排查方案
问题描述
编写Django项目的商品创建接口测试时,无论传入何种数据,测试始终返回400错误,预期应为201状态码。已确认权限无问题:authenticate(is_staff=True)在其他测试中正常生效,Staff账号可在Admin面板及前端正常提交商品。
环境版本
- Django 4.1.7
- Django Rest Framework 3.14.0
- pytest 7.2.2
- pytest-django 4.5.2
相关代码
test_products.py
from store.models import Collection, Product from rest_framework import status import pytest from model_bakery import baker @pytest.fixture def create_product(api_client): def do_create_product(product): return api_client.post('/store/products/', product) return do_create_product @pytest.mark.django_db class TestCreateProduct: def test_if_user_is_anonymous_returns_401(self, create_product): response = create_product({'title': 'a'}) assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_if_user_is_not_admin_returns_403(self, authenticate, create_product): authenticate() response = create_product({'title': 'a'}) assert response.status_code == status.HTTP_403_FORBIDDEN def test_if_data_is_invalid_returns_400(self, authenticate, create_product): authenticate(is_staff=True) response = create_product({'title': ''}) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data['title'] is not None def test_if_data_is_valid_returns_201(self, authenticate, create_product): authenticate(is_staff=True) response = create_product({ "title": "abc", "slug": "abc", "inventory": 1, "unit_price": 2, "collection": 1 }) assert response.status_code == status.HTTP_201_CREATED assert response.data['id'] > 0
conftest.py
from django.contrib.auth.models import User from rest_framework.test import APIClient import pytest @pytest.fixture def api_client(): return APIClient() @pytest.fixture def authenticate(api_client): def do_authenticate(is_staff=False): return api_client.force_authenticate(user=User(is_staff=is_staff)) return do_authenticate
ProductSerializer
class ProductSerializer(serializers.ModelSerializer): images = ProductImageSerializer(many=True, read_only=True) class Meta: model = Product fields = ['id', 'title', 'description', 'slug', 'inventory', 'unit_price', 'price_with_tax', 'collection', 'images'] price_with_tax = serializers.SerializerMethodField( method_name='calculate_tax') def calculate_tax(self, product: Product): return product.unit_price * Decimal(1.1)
ProductViewSet
class ProductViewSet(ModelViewSet): queryset = Product.objects.prefetch_related('images').all() serializer_class = ProductSerializer filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = ProductFilter pagination_class = DefaultPagination permission_classes = [IsAdminOrReadOnly] search_fields = ['title', 'description'] ordering_fields = ['unit_price', 'last_update'] def get_serializer_context(self): return {'request': self.request} def destroy(self, request, *args, **kwargs): if OrderItem.objects.filter(product_id=kwargs['pk']).count() > 0: return Response({'error': 'Product cannot be deleted because it is associated with an order item.'}, status=status.HTTP_405_METHOD_NOT_ALLOWED) return super().destroy(request, *args, **kwargs)
urls.py
from rest_framework_nested import routers from . import views router = routers.DefaultRouter() router.register('products', views.ProductViewSet, basename='products') router.register('collections', views.CollectionViewSet) router.register('carts', views.CartViewSet) router.register('customers', views.CustomerViewSet) router.register('orders', views.OrderViewSet, basename='orders') products_router = routers.NestedDefaultRouter( router, 'products', lookup='product') products_router.register('reviews', views.ReviewViewSet, basename='product-reviews') products_router.register( 'images', views.ProductImageViewSet, basename='product_images') carts_router = routers.NestedDefaultRouter(router, 'carts', lookup='cart') carts_router.register('items', views.CartItemViewSet, basename='cart-items') # URLConf urlpatterns = router.urls + products_router.urls + carts_router.urls
报错信息
====================================================== FAILURES ======================================================= _________________________________ TestCreateProduct.test_if_data_is_valid_returns_201 _________________________________ self = <test_products.TestCreateProduct object at 0x103a67390> authenticate = <function authenticate.<locals>.do_authenticate at 0x104a0e340> create_product = <function create_product.<locals>.do_create_product at 0x104a0e480> def test_if_data_is_valid_returns_201(self, authenticate, create_product): authenticate(is_staff=True) response = create_product({ "title": "abc", "slug": "abc", "inventory": 1, "unit_price": 2, "collection": 1 }) > assert response.status_code == status.HTTP_201_CREATED E assert 400 == 201 E + where 400 = <Response status_code=400, "application/json">.status_code E + and 201 = status.HTTP_201_CREATED store/tests/test_products.py:47: AssertionError ------------------------------------------------ Captured stderr call ------------------------------------------------- Bad Request: /store/products/
排查与解决思路
1. 确认测试数据库中存在对应Collection
测试中传入"collection": 1,但pytest的测试数据库是全新初始化的,默认不存在ID为1的Collection实例。可以用model_bakery提前创建Collection,再使用其真实ID:
def test_if_data_is_valid_returns_201(self, authenticate, create_product): authenticate(is_staff=True) # 创建一个Collection并保存到测试数据库 collection = baker.make(Collection) response = create_product({ "title": "abc", "slug": "abc", "inventory": 1, "unit_price": 2, "collection": collection.id # 使用实际存在的ID }) assert response.status_code == status.HTTP_201_CREATED assert response.data['id'] > 0
2. 打印400响应的具体错误详情
在测试中添加print(response.data),可以直接看到序列化器验证失败的具体字段和原因,快速定位问题:
def test_if_data_is_valid_returns_201(self, authenticate, create_product): authenticate(is_staff=True) response = create_product({ "title": "abc", "slug": "abc", "inventory": 1, "unit_price": 2, "collection": 1 }) print(response.data) # 输出错误信息 assert response.status_code == status.HTTP_201_CREATED assert response.data['id'] > 0
常见错误场景:
- Collection外键不存在
- 模型中必填字段未传入(比如
description如果是必填项,测试数据中未提交就会触发验证失败)
3. 检查Product模型的字段约束
核对Product模型中各字段的blank、null配置,如果description等字段设置为blank=False(必填),则测试数据中必须包含该字段:
response = create_product({ "title": "abc", "slug": "abc", "description": "测试商品描述", # 补充必填字段 "inventory": 1, "unit_price": 2, "collection": collection.id })
4. 优化authenticate fixture的用户创建逻辑
当前fixture中创建的User未保存到数据库,虽然DRF的force_authenticate支持未保存用户,但部分场景可能存在隐性问题。修改fixture保存用户:
@pytest.fixture def authenticate(api_client): def do_authenticate(is_staff=False): user = User(is_staff=is_staff) user.save() # 将用户保存到测试数据库 return api_client.force_authenticate(user=user) return do_authenticate
内容的提问来源于stack exchange,提问作者Thiago Guedes Willecke
相关产品推荐
相关产品推荐

