You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 08:13:09