ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Boot餐饮调度系统测试用例设计:覆盖核心模块与并发场景

Spring Boot餐饮调度系统测试用例设计:覆盖核心模块与并发场景 1. 餐饮调度系统的功能全貌与测试范围界定想给一套系统写测试用例第一步不是打开IDE敲代码而是先想清楚这套系统到底“管”什么。很多人一上来就盯着接口文档一个个列用例最后产出一堆“能跑但没价值”的脚本——因为你不了解业务边界就抓不住风险点。基于Spring Boot的餐饮调度系统名字听着不大实际拆开看里面塞的东西真不少。1.1 系统核心模块与业务链路梳理餐饮调度系统的业务核心是“调度”二字。它不是常规的点餐收银系统而是把前厅订单、后厨制作、骑手配送、桌台资源、排队叫号这几个环节串起来形成一个实时联动的闭环。我从功能模块维度拆一下大家看系统长什么样模块名称核心职责典型业务动作涉及关键数据菜品与库存管理菜品上下架、库存扣减与回补菜品售罄、估清、库存预警菜品ID、库存量、预警阈值桌台与排队管理桌台状态流转、线上排队取号入座、换桌、排队、叫号桌台编号、排队号、预计等待时长订单调度中心订单创建、拆分、状态流转用户下单、后厨接单、出餐订单号、订单状态、创建时间后厨制作调度按制作时长与优先级排序制作队列、超时催单、并单处理制作状态、预计出餐时间、优先级配送调度智能派单、骑手路径跟踪抢单、派单、送达确认骑手位置、配送距离、预计送达时间用户与权限管理多角色登录与数据权限隔离顾客、服务员、厨师、骑手、管理员登录角色、Token、权限集这里我特意用表格列出来是因为后文的测试用例全部围绕这些模块展开。注意看最后一行“用户与权限管理”——很多人在写测试用例时容易忽略这个模块但在实际项目中因为一个萝卜一个坑的角色权限漏洞被薅羊毛的例子太多了。后面我会专门用一节的篇幅讲权限测试怎么设计。从业务链路的视角看一条完整链路是这样的顾客在小程序点餐 → 订单落到调度中心 → 后厨屏显示制作任务 → 制作完成推送给配送调度 → 骑手接单送达。这条链路里任何一环状态不一致都会导致“用户等半天没饭吃”或者“骑手白跑一趟”。所以测试用例设计的核心策略就是围绕状态流转写覆盖围绕并发冲突写压力围绕异常分支写兜底。1.2 Spring Boot技术栈下的测试重点Spring Boot在这套系统里承担的是后端服务主干。基于它的技术特点写用例时有一些必须额外关注的测试维度第一分层架构的接口测试粒度。这套系统是典型的Controller-Service-DAO三层。测试用例不能只打在HTTP接口层还需要覆盖Service层的业务规则校验、事务回滚以及DAO层的SQL边界。比如说库存扣减如果设计成先查库存再扣减中间有并发插入就很容易超卖。这种问题在纯接口测试里很难触发必须把用例下沉到Service层配合隔离级别做并发模拟。第二Redis在会话状态与队列场景中的作用。餐饮调度场景里有大量临时状态比如排队号、骑手位置、后厨制作队列这些通常会放到Redis里。Redis本身不是强一致性的当Redis缓存和数据库数据不一致时会产生什么业务后果这是测试用例要重点设计的地方。比如用户排队取号后队列状态在Redis里更新了但数据库的桌台状态还没变重启应用后队列丢了怎么处理第三Spring Boot Actuator与可观测性。现在Spring Boot项目基本标配micrometer和actuator用来暴露健康检查、指标监控信息。从测试角度看这些端口本身就是攻击面。后面接口测试部分我会展示如何用MockMvc验证Actuator端点是否按预期暴露尤其是生产环境常见的“端点全裸奔”问题。第四定时任务与异步调度的正确性验证。餐饮调度里有大量定时任务比如超时未支付自动取消订单、高峰期自动调整排队叫号频率、配送超时自动重新派单。这些定时任务用的多是Scheduled注解测试用例里如果只是纯手工点接口根本触发不了怎么验证定时任务在正确的时间点正确执行是Spring Boot项目特有的挑战。2. 测试用例设计的核心方法与实操思路测试用例不是越多越好。我以前见过有人给一个登录功能写了100多条用例80%是重复的边界值。有效用例讲究的是覆盖度、成本、可执行性的平衡。餐饮调度系统业务交互复杂设计用例之前需要先把方法定下来。2.1 基于业务流的用例设计法最推荐的方法是基于业务流设计也叫“场景法”。它的核心思想是从用户的真实操作路径出发把系统里的链路走一遍每个关键节点再衍生出正常分支、异常分支和边界分支。拿“用户点餐”这条流来说一个最小化路径是用户进入菜单页 → 选择菜品 → 加入购物车 → 提交订单 → 支付 → 后厨接单 → 出餐 → 配送。这个链路里的每一个步骤都可以作为用例的锚点正常流全链路畅通订单状态依次流转最终送达。分支流支付超时取消订单、后厨拒单、骑手抢单后取消。异常流用户下单后库存不足被退回、配送中途取消订单。循环流用户取消订单后重新下单系统状态能否回到初始可用状态。我把这种想法落到一张场景矩阵表里写用例时直接对着表格展开就行主流程节点正向场景反向场景边界场景进入菜单页菜单菜品正常展示菜单接口超时降级菜品数量为0时的展示加入购物车正常加入、数量修改菜品下架后无法加入购物车达到上限99件提交订单生成订单且金额正确订单提交失败无脏数据菜品价格变动后的金额计算在线支付支付成功回调更新状态回调超时、签名错误支付金额与订单金额不一致后厨接单接单后状态推进同时多人操作同一订单超过N分钟未接单自动提醒骑手配送接单、取餐、送达状态流转配送超时自动改派骑手距店距离为0的特殊场景这个矩阵的好处是每个业务节点都至少对应一组正、反、边界用例测试思路不会漏。你不需要用工具纸和笔就可以开始设计自己的场景矩阵。2.2 数据准备与参数化技巧餐饮调度系统的测试用例有个特殊之处数据的业务含义太强了。同样是“创建订单”测试数据和业务真实数据的差别直接决定了用例能不能发现问题。很多测试团队的用例库看起来是全的但跑一遍全是无效执行就是因为造数据太随意。参数化设计注意两点第一用枚举类定义测试数据的“业务角色”。我习惯把测试数据分成几个固定集基础主数据菜品、桌台、骑手账号、流程状态数据各种中间状态的订单、边界特征数据库存临界值、最大排队人数、金额边界值。每个数据文件里写明前置条件比如“库存为3”的菜品是为“库存扣减到0后自动估清”这个场景准备的。这样用例和数据是解耦的换一套环境不会全崩。第二数据生成要考虑“脏数据”场景。比如下单接口除了正常的菜品ID还必须有“已被删除的菜品ID”、“库存为0的菜品ID”、“归属于其他门店的菜品ID”。这些数据是接口容错能力的重要验证点。执行用例时把这些数据放到一个参数化列表里同一个用例就能跑出多个分支。2.3 场景矩阵与并发冲突用例设计单用户的功能场景覆盖完整只是第一步。餐饮调度系统的核心难点是多角色并发操作同一份业务数据。顾客在点餐厨师在处理制作队列服务员在给桌台换位骑手在更新配送状态这些操作很多都作用在同一个订单或同一个桌台上。并发用例设计的核心是找出系统中的“竞争资源”。我总结了一套经验先看数据库表结构找唯一索引和更新密集的字段再看Redis key找所有并发写同一个key的场景最后看消息队列找可能被重复消费的事件。以桌台状态为例两个服务员同时操作桌台A一个执行“入座”一个执行“清洁完成”最终桌台A的状态取决于最后一个写库的请求。这个结果在并发业务下是“不可预知”的我们需要有逻辑去规避这种脏状态。测试用例就要设计两个服务员的请求近乎同时到达校验系统是否出现状态覆盖、是否触发乐观锁异常、是否给用户正确提示。我给这个环节总结出一个“并发场景清单模板”同一订单并发支付与取消。同一桌台并发入座与换桌。同一菜品并发下单与库存扣减。同一骑手并发接单与改派。同一用户并发重复提交订单。同一菜品并发估清与补货。每个场景至少写一条用例验证点集中在一件事后写是否覆盖先写、锁是否起作用、最终数据是否一致。这是调度系统质量的核心也是测试用例中最能体现测试设计价值的地方。3. 核心模块测试用例实例与模板参考方法聊完了上实操的东西。这一节我会给出一批可以直接抄作业的用例模板覆盖餐饮调度系统里几个核心模块。注意我给的模板不是让你原封不动拿去用而是理解字段关系和场景覆盖思路后改造成自己项目的用例。3.1 标准测试用例模板与字段说明一个标准的功能测试用例模板字段不宜过多多了维护成本高改一个需求全体翻车也不能太少少了关键信息缺失跑的人不知道前置条件是什么。我习惯用一组精简但够用的字段字段名填写说明示例用例编号模块前缀-数字编号保证唯一order_create_001用例名称一句话说明测什么正常创建订单且金额正确前置条件执行前必须满足的状态用户已登录已添加菜品到购物车测试数据参数化的数据描述菜品ID1001数量2库存5操作步骤可执行的逐步操作调用POST /api/order预期结果明确、可判定的结果描述返回订单ID订单状态为CREATED实际结果执行后填写核心是提供证据返回200订单ID12345优先级P0/P1/P2方便回归时筛选P1核心流程阻塞用例字段本身不重要重要的是预期结果要写“可以判定”的话。比如“系统应显示菜单列表”这种不算合格的预期结果而“返回菜单列表数组且包含菜品名称、价格、库存状态字段名称长度不超过20字符”才算合格。预期结果写得越具体测试执行时发现问题的概率越高。3.2 预订桌台模块用例示例预订桌台是调度系统里状态流转最复杂的模块之一也是线上bug的重灾区。我给出三组有代表性的用例第一组直接用表格展示方便大家对比理解。用例编号用例名称前置条件测试数据操作步骤预期结果table_reserve_001正常预订可用桌台门店营业中桌台A状态为空闲桌台A编号T001预订时段为18:00-19:00调用预订接口提交桌台ID和时段预订成功返回预订号桌台状态变为“已预订”table_reserve_002预订已被占用的桌台桌台B已经被其他顾客预订同一时段桌台B编号T002时段18:00-19:00调用预订接口返回错误码409提示“该时段已被占用”table_reserve_003边界时段预订桌台C在17:30-18:30被占用预订时段为18:30-19:00调用预订接口预订成功不与其他预订冲突table_reserve_004并发预订同一桌台两个用户同时对桌台D发起预订桌台D编号T004时段一致两个线程同时调用预订接口仅一个预订成功另一个收到冲突提示table_reserve_005不可预订已停用桌台桌台E被管理员设为停用状态桌台E编号T005调用预订接口返回错误码403提示“该桌台不可预订”这里的并发用例table_reserve_004看起来简单实际是最容易出问题的。如果代码里没有针对“桌台ID时段”建唯一索引也没有用分布式锁控制预订操作并发场景下就可能出现两条预订请求同时通过校验导致超卖。所以这个用例的预期结果必须是“仅一个成功”不是“两个都成功然后靠后写覆盖”。为了让这段可执行贴一段Spring Boot下用MockMvc做并发预订验证的示例代码Test void reserveSameTableConcurrently_shouldAllowOnlyOne() throws Exception { // 初始化一个可用桌台 Table table testDataFactory.createAvailableTable(T-001); String url /api/table/reserve; int threadCount 5; ExecutorService executor Executors.newFixedThreadPool(threadCount); CountDownLatch latch new CountDownLatch(1); AtomicInteger successCount new AtomicInteger(); AtomicInteger conflictCount new AtomicInteger(); for (int i 0; i threadCount; i) { executor.submit(() - { try { latch.await(); mockMvc.perform(post(url) .contentType(MediaType.APPLICATION_JSON) .content({\tableId\: table.getId() , \slot\: \2024-05-20 18:00-19:00\})) .andExpect(result - { if (result.getResponse().getStatus() 200) { successCount.incrementAndGet(); } else if (result.getResponse().getStatus() 409) { conflictCount.incrementAndGet(); } }); } catch (Exception e) { throw new RuntimeException(e); } }); } latch.countDown(); executor.shutdown(); executor.awaitTermination(10, TimeUnit.SECONDS); assertEquals(1, successCount.get()); assertEquals(4, conflictCount.get()); }这段代码的思路是用CountDownLatch让所有线程同时发出请求模拟真实并发。你可以用jmetter做同样的压力测试但单元测试层面的并发验证能更早发现问题而且方便集成进CI流程。注意这里我用的是MockMvc模拟HTTP层如果你的Service层已经有事务边界也可以直接在Service层做并发调用效果更纯粹。3.3 订单调度模块用例示例订单调度是整个系统的主动脉从生成到完成颜色太多。下面贴一份“订单状态流转”的用例集覆盖正常路径和几个高频异常重点看状态机的边界用例编号用例名称前置条件操作步骤预期结果order_flow_001正常订单全流程用户登录、菜品库存充足、骑手在线下单→支付→后厨接单→出餐→配送→送达订单状态按CREATED→PAID→ACCEPTED→COOKING→DELIVERING→COMPLETED有序流转order_flow_002重复支付同一订单订单已支付成功再次调用支付接口返回成功系统不重复扣款不改变订单状态返回幂等结果order_flow_003支付后库存扣减失败支付成功但库存服务异常观察订单状态订单状态回滚或进入待处理不允许出现已支付但无库存的订单order_flow_004后厨接单与用户取消竞争用户提交取消申请的同时后厨接单同时触发操作系统有明确状态判断不允许接单后订单被静默取消order_flow_005配送超时自动重新派单骑手长时间未接单等待超时阈值触发系统自动改派且原骑手不再收到该单推送order_flow_006订单取消后库存回补订单支付成功用户申请取消取消通过后检查菜品库存库存数量恢复为下单前数值记录回补流水订单状态流转里最恶心的坑是“死状态”比如订单已经COMPLETED但配送系统还推送着位置更新或者订单已经CANCELLED后厨端的屏幕还停留在制作中。写用例的时候每个状态流转都要配套一条“非法流转被拦截”的用例比如尝试从PAID直接跳到COMPLETED系统必须拒绝。关于库存回补order_flow_006这里有一个一定不要忘的场景如果取消请求和配送完成请求并行库存是回补还是不回补业务上应该是配送完成后库存不能回补但系统的实现逻辑必须是明确的不能看运气。这类“并行请求导致状态不一致”的场景恰恰是集成测试用例的重点设计对象。3.4 配送调度模块用例示例配送调度模块最小的核心是派单规则。我给出一个覆盖正常、异常、边界三种场景的表格重点是“边界条件”的细节因为派单规则最容易在边界上翻车用例编号用例名称前置条件操作步骤预期结果dispatch_001给平均接单量最少的骑手派单有3个骑手在线接单量分别为5、3、8新订单触发派单订单派给接单量为3的骑手dispatch_002给距离最近的骑手派单骑手A距离1km骑手B距离0.5km新订单触发派单订单派给骑手Bdispatch_003天气恶劣时调整派单半径系统开启恶劣天气模式新订单触发派单派单半径扩大超时阈值放宽用例记录调整结果dispatch_004无骑手在线时的降级策略所有骑手离线新订单触发派单生成待派单记录不抛异常不丢单dispatch_005骑手接单与系统改派竞争系统判定超时要改派骑手同时点击接单手动/并发触发系统有唯一性校验不允许同一订单被两个骑手同时持有配送调度里最典型的线上故障是把一个订单同时派给了两个骑手其实核心原因是接单接口和改派逻辑没做好锁控制。测试用例的重点不在“派给谁”而在“同一时刻只能有一个持有者”。这类用例建议在Service层用基于数据库唯一索引或Redis锁的方式做并发验证比单纯靠前端按钮防抖可靠得多。4. 接口测试、自动化落地与监控验证功能用例写得再漂亮最终也要落到自动化执行上。Spring Boot生态对接口测试的支持是相当友好的这一节我从框架选型、代码示例到监控验证完整走一遍实操流程。4.1 Spring Boot测试栈与依赖配置要搭建一套完整的Spring Boot接口测试环境核心依赖就三个spring-boot-starter-test提供JUnit 5、AssertJ、MockMvc等基础能力spring-boot-starter-web不用多讲spring-boot-starter-actuator负责运行时健康检查和指标暴露这一步对接口测试来说价值在于验证应用状态比如依赖的数据库是否就绪。建议再加上testcontainers用于跑集成测试时启动真实的MySQL和Redis容器避免依赖本地环境。以Maven为例pom.xml里基础配置长这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdorg.testcontainers/groupId artifactIdjunit-jupiter/artifactId scopetest/scope /dependency dependency groupIdorg.testcontainers/groupId artifactIdmysql/artifactId scopetest/scope /dependency这里有一个常见的坑如果你的工程里引入了spring-boot-starter-security那么测试用例跑接口时默认会被安全拦截挡在门外。在测试配置里要么放行所有请求要么在用例里构造一个mock用户。我一般是在SpringBootTest的测试配置类里加一个SecurityFilterChain的测试Bean放开csrf和匿名访问让用例专注在业务逻辑而不是安全配置上。4.2 核心接口测试用例的代码化示例用MockMvc写接口测试本质上就是把手工用例转成可重复执行的代码。下面用订餐流程里最常见的“创建订单”接口做一个示例包含接口返回码校验、核心字段校验和异常场景校验SpringBootTest AutoConfigureMockMvc class OrderApiTest { Autowired private MockMvc mockMvc; Autowired private ObjectMapper objectMapper; Test void createOrder_whenPayloadValid_shouldReturnOrderId() throws Exception { // 前置数据用户登录获取token购物车里有2个菜品 String token givenUserLoggedIn(); CreateOrderRequest request new CreateOrderRequest(); request.setUserId(1001L); request.setItems(Arrays.asList( new OrderItemRequest(2001L, 2), new OrderItemRequest(2002L, 1) )); mockMvc.perform(post(/api/order) .header(Authorization, Bearer token) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) .andExpect(jsonPath($.orderId).isNumber()) .andExpect(jsonPath($.totalAmount).value(calculateExpectedAmount())) .andExpect(jsonPath($.status).value(CREATED)); } Test void createOrder_whenItemStockNotEnough_shouldReturnConflict() throws Exception { // 前置数据2003号菜品库存只有1但下单数量为5 String token givenUserLoggedIn(); CreateOrderRequest request new CreateOrderRequest(); request.setUserId(1001L); request.setItems(Collections.singletonList(new OrderItemRequest(2003L, 5))); mockMvc.perform(post(/api/order) .header(Authorization, Bearer token) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isConflict()) .andExpect(jsonPath($.message).value(菜品库存不足)); } Test void createOrder_whenUserNotExist_shouldReturnNotFound() throws Exception { String token givenUserLoggedIn(); CreateOrderRequest request new CreateOrderRequest(); request.setUserId(99999L); request.setItems(Collections.singletonList(new OrderItemRequest(2001L, 1))); mockMvc.perform(post(/api/order) .header(Authorization, Bearer token) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isNotFound()); } }写这类接口测试时我一般遵守三个原则用例之间数据独立。不依赖其他测试用例创建的数据每条用例自己准备前置数据避免执行顺序影响结果。断言的关键字段要能反映业务正确性。比如订单金额不要只断言“返回金额大于0”要按菜品单价和数量计算一遍精确到分验证。异常场景的响应体结构要保持一致。无论哪种异常返回体里都该有message和code字段。这也是接口层的通用契约用例里的jsonPath才能统一。4.3 引入Micrometer与Actuator验证接口健康状态Spring Boot项目标配了micrometer和actuator之后接口测试能做的事就不仅是断言返回数据了还能验证整个应用的健康状态和接口性能指标。我建议在接口测试的集成测试阶段增加一个“冒烟测试类”专门做三件事调用/actuator/health断言UP状态。调用/actuator/metrics/http.server.requests验证核心接口的请求计数和耗时统计存在。检查/actuator/env和/actuator/beans是否未暴露在公网环境防止端口裸奔。一个典型的健康检查用例Test void healthEndpoint_shouldReturnUp() throws Exception { mockMvc.perform(get(/actuator/health)) .andExpect(status().isOk()) .andExpect(jsonPath($.status).value(UP)); } Test void metricsEndpoint_shouldRecordHttpRequests() throws Exception { mockMvc.perform(get(/actuator/metrics/http.server.requests)) .andExpect(status().isOk()) .andExpect(jsonPath($.measurements[0].statistic).exists()); }在真实的调度系统里我会习惯把/api/order、/api/table/reserve几个核心接口的请求量、耗时时长、错误率做成Dashboard每次跑完一轮接口回归就刷一次。这个也许不需要写进测试用例表里但是对质量建设来说是重要的加分项。测试用例的目标不只是发现bug也是在给系统的可观测性收集样本。4.4 通过Maven命令行一键执行测试集当测试用例文件多了单个IDE跑显然不现实。我习惯直接使用Maven命令行把整套用例集挂到CI流水线里。先推荐几个最常用的命令# 只跑单元测试跳过集成测试 mvn test # 跑所有测试包括Spring Boot集成测试和Testcontainers容器测试 mvn verify # 指定特定测试类和测试方法 mvn test -DtestOrderApiTest mvn test -DtestOrderApiTest#createOrder_whenPayloadValid_shouldReturnOrderId # 跳过测试只打包(日常不推荐但排查环境问题时很有用) mvn package -DskipTests这里有一个实用性建议在pom.xml里把集成测试和单元测试分开配置用failsafe-plugin跑集成测试、surefire-plugin跑单元测试。这样日常开发只需要跑surefire几百条用例几秒钟就出结果而完整的回归测试跑verify启动容器做全链路验证。两套测试的层次清晰跑起来心理负担也小。我之前在多地环境排查过一个问题本地测试全绿但CI上总挂在同一个用例。最后定位到原因是CI上application.yml里Redis地址指向了一个不可用的实例测试期间Redis连接建立失败导致队列相关测试失败。这种情况如果测试代码里没用Testcontainers统一环境你会在环境配置上浪费大量时间。建议所有涉及外部中间件的测试直接用Testcontainers起真实容器或者至少在测试配置里用MockBean把外部依赖Mock掉保证用例跑的环境是一致的。4.5 接口安全与权限测试用例权限是餐饮调度系统里特别容易漏测但危害极大的模块。用户、服务员、厨师、骑手、管理员这五个角色的权限边界必须清晰否则就是真实事故。举几个真实发生过的失控场景普通用户调用后厨接口看到了所有订单骑手调用管理员接口把别人的订单改派了服务员通过直接请求URL绕过前端按钮看到了不属于自己门店的数据。权限测试用例设计核心不是“能访问什么”而是“不能访问什么”。我常用的方法是给每个接口建立一张“角色权限矩阵表”然后针对“无权限访问”写透用例接口路径匿名用户顾客服务员厨师骑手管理员POST /api/order401200403403403403GET /api/kitchen/tasks401403403200403200POST /api/dispatch/reassign401403403403403200PUT /api/table/status401403200403403200每个单元格就是一条用例。比如GET /api/kitchen/tasks用顾客身份访问预期结果是403。如果返回了200那说明接口的鉴权注解漏了这种问题在冒烟测试阶段就能抓到。权限测试用例还有一个容易漏的地方路径穿越式越权。用户A登录后把请求里的userId改成用户B的ID能不能看到别人的订单这属于数据越权这种问题的本质是后端只校验了“登录了”没校验“这个资源属于当前用户”。这类用例务必加进去而且每个资源类的接口都要测。5. 八大高频问题与排查策略实录最后分享一批我在实际测试和线上问题排查中踩过的坑、总结的经验。这些问题大多不是单个接口的问题而是多个服务、多个线程、多个组件交互时引发的“组合bug”。整理成速查表形式方便你直接查阅。问题现象可能原因定位思路解决/预防方案订单支付后库存仍被扣两次事务边界控制不当回补逻辑重复执行检查支付回调接口是否被多次调用查看订单状态更新的幂等控制消费端加幂等表/Redis分布式锁回调中先查状态再更新定时任务重复执行多实例部署时任务叠加Scheduled默认未做分布式锁查看应用日志中同一任务在不同实例上的执行时间引入ShedLock或Redisson分布式锁保证同一时刻单一节点执行Redis中排队队列与数据库状态不一致重启应用或Redis数据丢失对比Redis key和数据库表记录差异对关键状态以数据库为准Redis降级为热数据加速层Redis Stream消费组消息重复消费或堆积消费者处理成功后未确认查看消费组的pending列表和consumer数量正确配置手动ack消费成功后确认消息设置合理的重试机制和死信队列接口响应慢排查到数据库连接池耗尽连接泄漏或长事务占用通过actuator查看连接池指标分析慢SQL事务代码块尽量缩小范围用完连接及时释放并发派单给同一骑手重复订单抢单接口和派单接口缺少同一把锁检查SQL是否有唯一索引控制Redis锁是否在事务提交前释放用数据库唯一索引兜底Redis锁仅在业务处理期间持有权限控制只在页面隐藏接口裸奔可访问后端接口缺少鉴权注解用角色矩阵逐个接口扫描统一走Spring Security方法级鉴权禁止依赖前端显示控制测试环境数据污染跑完用例后数据错乱用例之间没有数据隔离检查测试用例是否清理了数据、是否用了固定ID测试数据统一前缀用例结束清理容器化环境用Testcontainers快速重置这里挑两个重点展开一下因为它们是调度系统特有的“坑王”。第一个是Redis Stream消费组的消息确认问题。餐饮调度系统里有不少异步操作是通过Redis Stream做消息传递的比如“订单创建后通知后厨大屏”、“支付成功后触发库存扣减任务”。如果消费逻辑里忘了手动确认消息或者业务异常时消息进入了pending状态没有处理最常见的现象是部分订单“凭空消失”——后厨没看到新单但用户已经支付成功了。测试用例里一定要设计“消费失败后重试”和“pending消息兜底恢复”这两个场景否则这种问题上线后很难发现。第二个是同一订单并发状态更新导致的多写覆盖。我之前遇到一个真实事故用户取消订单配送系统同时确认送达两个线程同时读到订单状态是DELIVERING一个更新为CANCELLED一个更新为COMPLETED最后的结果取决于最后一个提交的事务完全不可控。后来我们应对的方案是在订单表增加版本号字段用乐观锁保证状态更新时版本匹配才允许写入同时在代码里对关键状态流转加了校验禁止从CANCELLED流转到任何其他状态。对应到测试用例就是并发取消与送达的用例预期结果必须是“只有一个操作生效”而不是“两个都成功”。6. 提升用例稳定性的三个小技巧最后分享三个不太容易在教科书里看到但在项目里实际非常管用的技巧。第一个是关于用例失败时的证据收集。很多测试人员跑完用例看到红色FAIL就截图提bug开发问你“哪一步出的错”答不上来。我建议在用例的断言之外加一个通用的“失败现场记录器”测试用例抛异常时自动截取当前页面状态或接口响应体、数据库关键表的数据快照、以及日志文件中最近50行日志。这些证据打包输出到测试报告里开发和测试沟通效率能提高一大截。第二个是关于用例可重复执行的“幂等设计”。测试用例跑一次成功不算本事跑十次结果一样才算稳定。开发环境的数据是共享的如果用例里创建的数据没有唯一性标识第二次执行时可能就撞上主键冲突。建议把所有测试数据都加上UUID前缀用完即清。代码骨架大致是String uniqueSuffix UUID.randomUUID().toString().substring(0, 8); String userName test_user_ uniqueSuffix;这个小习惯能帮你减少大量“换环境重跑用例”的无效工作。第三个是避免用“固定时长等待”来测试异步任务。很多新人写异步任务用例习惯Thread.sleep(3000)然后断言结果这种用例十有八九是不稳定的服务器一忙就翻车。更好的方案是用Awaitility等库写成“最多等待10秒每100毫秒轮询一次直到满足条件”。这样用例跑得快邹也更稳await().atMost(10, TimeUnit.SECONDS) .untilAsserted(() - assertEquals(COOKING, orderService.getOrder(orderId).getStatus()));这个技巧在写定时调度的验证用例时尤其好用从“赌时间”变成“等状态”稳定性和效率都有改善。我在实际项目里体验最深的一点是测试用例的维护成本往往大于最初编写用例的成本。归档好用例、保持用例和数据的环境隔离、把接口断言写得足够精确这些前期“慢功夫”会在项目进入稳定期后释放出巨大的红利。餐饮调度系统说到底是一个多角色、高并发、强状态流转的系统这种系统的测试用例永远要围绕着“状态的一致性”去展开——订单状态、桌台状态、配送状态任何一个状态混乱用户感受到的就是“你的系统出了问题”。希望这一篇结合Spring Boot技术栈的用例设计与落地经验能帮你在自己的调度系统测试里少踩几个坑。
返回列表