ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

零基础入门python43:Django接口测试与库存边界

零基础入门python43:Django接口测试与库存边界 零基础入门python43Django接口测试与库存边界一、上一篇课后练习讲解管理员查看全部订单应使用独立的权限类和 ViewSet不能修改普通用户的get_queryset。权限越宽的接口越应该单独命名、单独测试。上一篇课后练习完整答案上一篇练习的要求已落实到下面完整文件先运行项目测试再用 curl 对照状态码和数据库持久化结果答案要点OrderViewSet 的 get_queryset 按 staff 或 user 分支详情和 action 均调用 get_object普通用户看不到他人订单。文件shop/views.py完整参考答案文件完整文件shop/views.pyfromrest_frameworkimportviewsetsfromrest_framework.permissionsimportIsAuthenticatedfrom.modelsimportOrderclassOrderViewSet(viewsets.ReadOnlyModelViewSet):permission_classes[IsAuthenticated]defget_queryset(self):qsOrder.objects.all()ifnotself.request.user.is_staff:qsqs.filter(userself.request.user)returnqs.order_by(-created_at)完整参考答案文件本篇对应的交付源码完整文件django-shop/shop/views.pyfromrest_frameworkimportgenerics,permissions,status,viewsetsfromrest_framework.responseimportResponsefrom.modelsimportCategory,Order,Productfrom.serializersimportCategorySerializer,CheckoutSerializer,OrderSerializer,ProductSerializerfrom.servicesimportcreate_orderclassCategoryViewSet(viewsets.ReadOnlyModelViewSet):querysetCategory.objects.order_by(name)serializer_classCategorySerializerclassProductViewSet(viewsets.ReadOnlyModelViewSet):serializer_classProductSerializerdefget_queryset(self):queryProduct.objects.filter(is_activeTrue).select_related(category)ifcategory:self.request.query_params.get(category):queryquery.filter(category_idcategory)ifkeyword:self.request.query_params.get(q):queryquery.filter(name__icontainskeyword)returnquery.order_by(-created_at)classCheckoutView(generics.GenericAPIView):permission_classes(permissions.IsAuthenticated,)serializer_classCheckoutSerializerdefpost(self,request):serializerself.get_serializer(datarequest.data)serializer.is_valid(raise_exceptionTrue)ordercreate_order(request.user,serializer.validated_data[items])returnResponse(OrderSerializer(order).data,status201)classMyOrderViewSet(viewsets.ReadOnlyModelViewSet):permission_classes(permissions.IsAuthenticated,)serializer_classOrderSerializerdefget_queryset(self):returnOrder.objects.filter(userself.request.user).prefetch_related(items).order_by(-id)classCartView(generics.GenericAPIView):defget(self,request):returnResponse(request.session.get(cart,{}))defpost(self,request):productgenerics.get_object_or_404(Product,idrequest.data.get(product_id),is_activeTrue)quantityint(request.data.get(quantity,1))ifquantity1orquantityproduct.stock:returnResponse({message:数量必须大于0且不能超过库存},statusstatus.HTTP_400_BAD_REQUEST)cartrequest.session.setdefault(cart,{})cart[str(product.id)]quantity request.session.modifiedTruereturnResponse(cart,statusstatus.HTTP_201_CREATED)defdelete(self,request):cartrequest.session.get(cart,{})cart.pop(str(request.data.get(product_id)),None)request.session.modifiedTruereturnResponse(cart)验收命令python -m pytest -qDjango 项目使用 python manage.py test。预期测试通过若失败先检查迁移、配置和事务回滚。二、本篇测试什么用APITestCase验收公开商品、未登录结算、成功下单、库存不足回滚、购物车库存边界和用户订单隔离。三、测试示例deftest_insufficient_stock_rolls_back_order(self):self.client.force_authenticate(self.user)responseself.client.post(/api/checkout/,{items:[{product_id:self.product.id,quantity:9}]},formatjson)self.assertEqual(response.status_code,400)self.assertEqual(Order.objects.count(),0)self.product.refresh_from_db()self.assertEqual(self.product.stock,5)refresh_from_db()很重要测试内存中的对象可能仍是旧值必须重新从数据库读取才能验证事务结果。四、验收python manage.py test-v 0 python manage.py check预期 5 个测试全部通过、system check 无问题。课后练习增加重复点赞或重复下单边界测试并记录预期状态码。项目增量接口测试和库存边界测试不能只验证 200。库存为 0、数量为 0、重复下单、越权订单和事务中途异常都要覆盖尤其要查询数据库确认库存没有负数。responseclient.post(/api/orders/,{items:[{product_id:product.id,quantity:2}]})assertresponse.status_code409product.refresh_from_db()assertproduct.stock1使用 TransactionTestCase 才能测试真实锁行为普通 TestCase 的事务包装可能掩盖并发问题。课后练习模拟两个请求抢最后一件商品。五、上一篇练习讲解权限测试要和状态机测试分开staff 发货测试验证paid - shipped普通用户应得到 403订单所有权测试验证用户 B 得不到 A 的对象。分开写可以在失败时快速判断是 queryset 还是状态转换出错。六、库存边界的接口测试classOrderApiTests(APITestCase):deftest_out_of_stock_rolls_back(self):productProduct.objects.create(name鼠标,price20.00,stock1,categoryself.category)responseself.client.post(/api/orders/,{idempotency_key:str(uuid4()),items:[{product_id:product.id,quantity:2}],},formatjson)self.assertEqual(response.status_code,409)product.refresh_from_db()self.assertEqual(product.stock,1)self.assertEqual(Order.objects.count(),0)测试不仅看状态码还要查询数据库确认没有半张订单、库存没有负数。refresh_from_db()很重要否则测试内存中的product.stock可能仍是旧值导致假通过。七、并发测试为什么要 TransactionTestCaseTestCase会在外层包一层事务并回滚锁可能不会像生产一样释放并发场景请用TransactionTestCase通过两个线程或两个数据库连接发起请求。SQLite 无法模拟完整行锁建议在 PostgreSQL CI 服务上执行classConcurrentOrderTests(TransactionTestCase):reset_sequencesTrue# 使用 barrier 让两个事务同时尝试读取库存再观察只有一个成功。并发测试偶尔超时不应简单增大等待时间要检查锁顺序是否一致多个商品下单时按 id 排序后再锁行能够降低死锁概率。八、上一篇练习验收、常见错误与本篇练习运行python manage.py check python manage.py test shop.tests-v 2预期所有测试通过。IntegrityError未被转为 409 时检查 DRF exception handler出现库存负数检查是否使用了F(stock) - quantity并在锁内再次判断订单重复检查幂等键唯一约束是否已迁移。本篇练习模拟两个请求抢最后一件商品增加重复点赞/重复下单边界测试并把测试命令写入 README。下一篇将从空目录启动整个 Django 商城做最终验收。十、让测试失败信息可读为每个边界断言写清上下文例如assertEqual(product.stock, 1, 库存不足时不应被扣减)使用subTest(quantityquantity)批量覆盖 0、-1、100。测试失败时先保存请求体、用户角色和数据库快照再修改代码避免“改到绿但不知道为什么”。CI 中使用-v 2保留测试名称毕业报告附上命令和完整输出。接口测试还应检查响应头缓存策略、request id、Content-Type和错误结构。对于事务失败场景除了返回 409还要重新查询订单、订单项和库存只有持久化状态正确测试才真正证明回滚生效。本篇结束完整模块文件下面是交付项目中真实存在的完整文件 django-shop/shop/tests.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码复制单个函数会丢失上下文因此这里提供整份文件。fromdjango.contrib.authimportget_user_modelfromrest_framework.testimportAPITestCasefrom.modelsimportCategory,Order,ProductclassShopApiTest(APITestCase):defsetUp(self):self.userget_user_model().objects.create_user(usernamealice,passwordpassword123)categoryCategory.objects.create(name图书,slugbooks)self.productProduct.objects.create(categorycategory,namePython入门,slugpython,price59.00,stock5)deftest_products_are_public(self):responseself.client.get(/api/products/)self.assertEqual(response.status_code,200)self.assertEqual(response.data[0][name],Python入门)deftest_checkout_requires_login(self):responseself.client.post(/api/checkout/,{items:[{product_id:self.product.id,quantity:1}]},formatjson)self.assertEqual(response.status_code,403)deftest_session_cart_checks_stock(self):responseself.client.post(/api/cart/,{product_id:self.product.id,quantity:2},formatjson)self.assertEqual(response.status_code,201)self.assertEqual(response.data[str(self.product.id)],2)responseself.client.post(/api/cart/,{product_id:self.product.id,quantity:9},formatjson)self.assertEqual(response.status_code,400)deftest_checkout_creates_order_and_reduces_stock(self):self.client.force_authenticate(self.user)responseself.client.post(/api/checkout/,{items:[{product_id:self.product.id,quantity:2}]},formatjson)self.assertEqual(response.status_code,201)self.assertEqual(response.data[total_amount],118.00)self.product.refresh_from_db()self.assertEqual(self.product.stock,3)self.assertEqual(Order.objects.get().user,self.user)deftest_insufficient_stock_rolls_back_order(self):self.client.force_authenticate(self.user)responseself.client.post(/api/checkout/,{items:[{product_id:self.product.id,quantity:9}]},formatjson)self.assertEqual(response.status_code,400)self.assertEqual(Order.objects.count(),0)
返回列表