← 返回题目列表

责任链模式如何做单元测试和链路可观测性?

高频 中等 第 9 / 27 题 更新于 2026/08/02
责任链模式单元测试可观测性日志指标

简化版

责任链测试要分两层:单个处理器测试职责逻辑,整条链测试顺序、短路、异常和兜底。可观测性要记录链名、版本、处理器顺序、每个节点耗时、决策结果和短路原因,否则线上很难解释请求为什么被拦截或漏处理。

详细版

责任链比普通方法更需要测试和观测,因为它的行为由多个处理器共同决定。单测只测某个 Handler 不够,还要有集成式链路测试:合法请求走完整链,非法请求在指定节点短路,异常请求返回统一错误,空链或无人处理时有兜底。

可观测性方面,链容器应该统一埋点,而不是让每个处理器随意打日志。每次请求至少要知道:走的是哪条链、链路版本是多少、执行了哪些节点、每个节点耗时多少、最终结果是什么。如果责任链是动态配置的,还要能关联配置版本。

面试里可以回答“处理器单测 + 链路契约测试 + 顺序断言 + 短路断言 + 统一日志指标”。这比只说“写 JUnit”更完整。

完整版教学

一、为什么责任链特别需要测试

责任链的代码往往被拆成多个小处理器,每个处理器看起来都很简单,但组合后的行为可能复杂。比如 Auth -> Param -> Risk -> Stock -> Submit,单看每个节点都没问题,顺序调换后就可能出错:风控在参数规范化前执行,读到的字段为空;库存冻结在风控前执行,风控拒绝后还要补偿。

正确顺序:
Auth -> ParamNormalize -> Risk -> Stock -> Submit

错误顺序:
Auth -> Risk -> ParamNormalize -> Stock -> Submit

测试的目标不是追求覆盖率数字,而是把链路契约固定下来。假设 6 个处理器有 2 个短路节点、1 个异常节点,如果只测最终成功路径,至少会漏掉 3 类风险。

记忆钩子:责任链的 bug 经常不在单个处理器,而在处理器之间的顺序、短路和共享上下文。

二、单个处理器要测职责边界

每个处理器应该只测自己的职责,不要在单测里把整条链都拉进来。比如 AuthHandler 只负责未登录时停止、已登录时继续,并写入用户信息。可以用假的 Chain 判断它是否调用了下一步。

@Test
void shouldStopWhenUserNotLogin() {
    OrderContext context = new OrderContext(requestWithoutToken());
    FakeChain chain = new FakeChain();

    ChainResult result = authHandler.handle(context, chain);

    assertEquals(Status.STOP, result.status());
    assertFalse(chain.called());
}

单处理器测试至少覆盖 3 种情况:正常通过、业务短路、异常或边界输入。比如参数校验处理器要测缺字段、字段格式错误和合法参数;风控处理器要测低风险通过、高风险拒绝、风控服务超时。

三、整条链要测执行顺序和短路位置

链路测试重点是组合行为。可以使用记录型处理器,把每个节点执行时往列表里写名字,然后断言顺序。这样新增处理器或调整顺序时,测试能明确提示链路契约变化。

@Test
void shouldExecuteHandlersInExpectedOrder() {
    List<String> executed = new ArrayList<>();
    HandlerChain chain = new HandlerChain(List.of(
        new RecordingHandler("Auth", executed),
        new RecordingHandler("Param", executed),
        new RecordingHandler("Risk", executed)
    ));

    chain.handle(new OrderContext(validRequest()));

    assertEquals(List.of("Auth", "Param", "Risk"), executed);
}

短路测试也很重要。比如未登录请求应该只执行 Auth,不能继续执行 ParamRisk。如果断言只看最终结果是失败,就无法发现多执行了后续节点的问题。

未登录期望: Auth -> stop
实际错误: Auth -> Param -> Risk -> stop

四、异常和兜底要有专门测试

责任链里最容易漏的是异常路径和链尾兜底。比如所有处理器都选择 pass,最后没人处理请求,系统应该返回明确错误还是默认通过?这个要通过测试固定下来。异常处理也要验证链容器是否记录当前处理器、是否返回统一错误码。

测试场景期望
空链返回配置错误或默认通过,按业务契约
无人处理触发链尾兜底
中间节点抛异常返回统一 CHAIN_ERROR
补偿节点失败记录补偿任务
@Test
void shouldReturnChainErrorWhenHandlerThrows() {
    HandlerChain chain = new HandlerChain(List.of(new ThrowingHandler("Risk")));
    ChainResult result = chain.handle(new OrderContext(validRequest()));
    assertEquals("CHAIN_ERROR", result.code());
}

五、可观测性要放在链容器里

如果每个处理器自己打日志,格式很难统一,漏打也很常见。链容器天然知道当前执行的是哪个处理器、它前后耗时、返回结果和异常,所以统一埋点最合适。处理器只需要返回业务结果。

traceId=abc chain=orderSubmit version=v3 handler=Auth result=PASS costMs=2
traceId=abc chain=orderSubmit version=v3 handler=Param result=PASS costMs=1
traceId=abc chain=orderSubmit version=v3 handler=Risk result=STOP code=RISK_REJECT costMs=18

这类日志要能回答 4 个问题:请求走了哪条链,走到哪个节点停了,为什么停,每个节点慢不慢。没有这些信息,线上排查只能靠猜。

六、指标要区分链路级和节点级

日志适合排查单个请求,指标适合看整体趋势。链路级指标包括 QPS、成功率、短路率、错误率、耗时分位数;节点级指标包括每个处理器的通过数、拒绝数、异常数、平均耗时和 P95 耗时。

指标粒度示例用途
chain_total链路看订单链请求量
chain_stop_total链路看拦截率是否突增
handler_cost_ms节点找慢处理器
handler_error_total节点定位异常节点
handler_decision_total节点分析拒绝原因

假设风控节点 P95 从 20ms 涨到 200ms,而链路总 P95 从 80ms 涨到 260ms,就能判断主要瓶颈在风控节点,而不是整条链都慢。

七、动态链还要测试配置版本

动态装配的责任链需要额外测试配置解析和版本变更。比如配置里写了 Auth, Param, Risk,测试应断言最终生成的链就是这 3 个节点,并且顺序一致。配置缺少必选节点时,要在启动或加载阶段失败,而不是等请求进来才失败。

配置:
orderSubmit v5 = Auth(100), Param(200), Risk(300)

测试断言:
chain.name == orderSubmit
chain.version == v5
handlers == [Auth, Param, Risk]

如果支持灰度,还要测试同一个场景下不同用户命中不同版本。比如 10% 用户走 v6,其余走 v5,日志里必须带版本,否则排查结果会混在一起。

八、常见误区与追问

  • 误区:每个 Handler 单测过了,整条链就没问题。 责任链的核心风险在顺序、短路和上下文协作,必须有链路级测试。
  • 误区:只测成功路径。 短路、异常、空链、无人处理、补偿失败都属于高频问题。
  • 误区:日志放在每个处理器里更灵活。 统一链容器埋点才能保证格式、耗时和异常记录一致。
  • 追问:怎么测试处理器是否调用 next? 使用假的 Chain 或记录型处理器,断言后续节点是否被执行。
  • 追问:怎么定位线上被哪个节点拦截? 日志里记录 chainName/version/handler/decision/code/costMs,按 traceId 串起来。
  • 追问:指标会不会太多? 处理器数量可控时按节点打指标很有价值,标签要限制在链名、节点名、结果这类低基数字段。

九、加强记忆

责任链测试和观测记住“两层测试、两级观测”:单处理器测职责边界,整条链测顺序短路;链路级观测看整体成功、失败和耗时,节点级观测定位哪个处理器慢、拒绝或异常。这样责任链不是一个看不见内部的黑箱,而是每次请求都能被解释的流程。