测试代码写到一定阶段几乎每个人都会撞上同一堵墙明明只是验证一个下单逻辑结果为了跑通测试硬生生把支付网关、短信服务、库存中心全给“造”了一遍。更让人抓狂的是团队里三个人对同一个假对象的叫法完全不同有人叫 mock有人叫 stub还有人管它叫 fake代码评审时鸡同鸭讲最后干脆全部叫“假数据”了事。这篇内容就想把这三个概念彻底掰开揉碎讲清楚它们各自解决什么问题、在什么场景下该用哪一个、用错了会埋下什么隐患。不管你是刚写第一行单元测试的新手还是已经维护着几千个测试用例、正被脆弱测试折磨的老手都能从这里找到可以直接抄作业的判断标准和落地写法。1. 三个概念为什么总被混为一谈1.1 从一次真实的代码评审说起我印象特别深的一次评审某同学提交了一个订单服务的测试代码里有个类叫MockPaymentGateway点进去一看它内部维护了一个固定的返回值列表每次调用按顺序吐出一个结果既不校验调用次数也不校验传参。这玩意儿从行为上看明明是个 stub但名字叫 mock。评审时另一位同学说“你这个 mock 写得不对应该验证一下调用”作者反驳“我这就是个假对象验证什么调用”。两个人吵了十分钟其实说的根本不是一回事。这个场景暴露的问题很典型概念的名字被当成了实现的名字。很多人是先学会了“mock 这个词”然后把自己写的所有测试替身都叫 mock至于它实际承担的是“提供数据”还是“验证交互”完全没想过。而测试替身Test Double这个词反而很少有人提它才是这三者的上位概念——所有用来替代真实依赖的对象统称测试替身fake、mock、stub 都是它的具体形态。所以理清概念的第一步是把层级关系摆正测试替身是总称下面按“替代目的”分成若干类。搞不清层级就会陷入“mock 和 stub 哪个更好”这种伪命题的争论因为它们压根不是竞争关系而是解决不同问题的工具。1.2 一个生活化的类比拍电影时的替身演员要理解这三者的区别用拍电影打比方最直观。Stub 像“台词提词器”。演员只需要照着提词器念出预设的台词提词器不关心演员念得对不对、念了几遍它只负责在需要的时候给出那句词。对应到代码里stub 就是“你调用我我给你一个事先准备好的返回值”仅此而已它不记录你调了几次也不检查你传了什么参数。Mock 像“片场导演”。导演不光要给演员台词还要盯着演员这场戏你必须在第 3 幕出现必须说这句台词说错了我就要喊卡。对应到代码里mock 的核心能力是验证交互——它记录被调用的次数、参数、顺序测试结束时断言“这个依赖是否按预期被调用了”。Fake 像“用道具替真车”。拍飙车戏不可能真开一辆法拉利去撞于是用一辆外观相似、能开动、但内部结构简化的道具车。它有自己的实现逻辑能真实地跑起来只是比真车简单。对应到代码里fake 就是一个有真实行为但实现简化的替代品最典型的就是内存数据库——它真的能存能取能查只是数据放在内存里而不是磁盘上。这个类比的关键在于三者不是精度高低的关系而是职责不同。提词器再高级也不会变成导演道具车再逼真也不负责检查演员走位。1.3 混用的代价测试脆弱与误报概念混用不只是命名难看它会直接导致测试质量问题最典型的有两类。第一类是测试过度耦合实现。用 mock 去验证“内部调用了某个依赖三次”一旦重构时把三次调用合并成一次批量调用功能完全没变测试却红了。这类测试看似严谨实则把实现细节焊死在了断言里重构成本极高。很多团队抱怨“测试一改就红、红了就得改测试”根源往往就是滥用交互验证。第二类是测试失去意义。用 stub 去替代一个本该验证交互的依赖比如“下单成功后必须调用一次库存扣减”结果 stub 只负责返回成功压根不检查有没有调用库存服务。测试永远是绿的但线上真出了“下单不扣库存”的 bug它一点都发现不了。这种测试给人虚假的安全感比没有测试更危险。提示判断一个测试替身用得对不对有个简单标准——问自己“如果这段被测代码的行为悄悄变了我的测试会不会红”。如果答案是“不会”那这个替身大概率用错了地方。2. 逐个拆解每种替身到底解决什么问题2.1 Stub只给答案不问过程Stub 的定位非常纯粹为被测代码提供可控的输入。它的存在是为了让被测逻辑能顺利往下走而不必依赖真实的外部环境。比如被测代码要调用一个汇率服务获取当前汇率测试里不可能真去请求外部接口于是给一个 stub固定返回 7.2这样被测代码就能拿着 7.2 继续算金额。写 stub 的关键在于“够用就好”。它不需要有内部状态不需要记录调用历史甚至不需要处理异常分支——除非你要测的就是异常分支。一个典型的 stub 长这样以常见的 Java 测试框架思路为例其他语言同理// 伪代码示意一个只负责返回固定值的 stub public class FixedRateStub implements RateService { Override public double getRate(String currency) { return 7.2; // 无论传什么币种都返回 7.2 } }这里有个容易踩的坑stub 的返回值要覆盖被测代码真正关心的分支。如果被测代码里有“汇率大于 7 走 A 逻辑小于 7 走 B 逻辑”那你至少得准备两个 stub 实例一个返回 7.5一个返回 6.8分别验证两条路径。只准备一个固定值等于只测了一半。另一个心得是stub 尽量写得“笨”一点。有些人喜欢在 stub 里加一堆 if-else 模拟各种场景结果 stub 本身变成了一个需要被测试的复杂对象。记住 stub 的使命是提供输入不是模拟整个世界。场景多就多写几个简单的 stub 实例而不是写一个万能 stub。2.2 Mock盯着你看你怎么做Mock 的核心是交互验证。它关心的不是“返回了什么”而是“被测代码有没有按约定去调用依赖”。典型场景是“副作用必须发生”——下单成功必须发一条消息、注册成功必须写一条审计日志、支付成功必须调用一次对账接口。这些行为的共同点是返回值可能不重要但“有没有做”极其重要。用 mock 时测试结构通常是“准备—执行—验证”三段式验证段专门断言交互// 伪代码示意验证库存服务被调用了一次且参数正确 Test public void should_deduct_stock_once_when_order_created() { StockService stockMock mock(StockService.class); OrderService orderService new OrderService(stockMock); orderService.createOrder(new Order(SKU-001, 2)); verify(stockMock, times(1)).deduct(SKU-001, 2); }这段代码的价值在于如果哪天有人把扣库存的逻辑删了或者改错了参数测试立刻红。但它的风险也在这里它把“调用一次、参数是这两个”写死成了契约。如果后续重构改成批量扣减一次调用传一个列表功能等价但测试必红。所以用 mock 要有节制只对真正重要的交互做验证别把每个依赖调用都 mock 一遍。我个人的经验是一个测试里 mock 的验证断言最好不超过两三条。超过这个数说明你在测试里描述的实现细节太多了测试会变得又长又脆。真需要验证很多交互往往意味着这段代码职责太重该考虑拆分了。2.3 Fake能跑起来的简化版Fake 是三者里最容易被低估、但实际价值最高的一种。它有真实的行为逻辑只是实现比生产版本简单。最经典的例子是内存数据库生产环境用真实的持久化数据库测试环境用一个基于哈希表实现的假数据库接口完全一致增删改查都能真实工作只是数据不落盘、重启即失。Fake 的最大优势是让测试更接近真实。用 stub 和 mock 时被测代码其实是在和“死”对象打交道返回什么全凭你设定而 fake 是“活”的你存进去什么读出来就是什么中间的逻辑真实运转。这让集成度更高的测试成为可能比如测试一个仓储层的完整读写流程用内存数据库 fake 就能跑通而不必连真实数据库。写 fake 的代价是要自己实现一套逻辑所以它适合接口稳定、行为相对简单的依赖。像缓存、消息队列、文件存储这类社区里往往已经有成熟的 fake 实现可以直接用。自己从零写 fake 时务必守住一条底线fake 的行为要和真实实现保持一致尤其是边界情况。如果 fake 对空值的处理和真实数据库不同那测试通过也说明不了问题反而会误导人。2.4 三者对比速查表把关键差异整理成一张表选型时对着看最省事维度StubMockFake核心职责提供预设返回值验证交互是否发生提供简化的真实行为是否记录调用否是通常否是否有内部逻辑极简或无无只记录有且能真实运转典型场景隔离外部依赖、构造输入验证副作用、关键调用替代数据库、缓存、队列主要风险返回值覆盖不全过度耦合实现细节行为与真实实现不一致维护成本低中中到高这张表不是让你死记而是帮你建立判断路径先问“我要验证的是返回值还是行为”再问“这个依赖需不需要真实运转”。两个问题答完用哪种基本就清楚了。3. 选型决策什么场景该用哪一个3.1 从“验证目标”倒推替身类型选型最靠谱的起点不是“哪个流行”而是“这个测试到底想证明什么”。我把常见的验证目标归成三类对应关系很清晰。第一类验证计算结果。被测代码拿到输入经过计算产出一个结果。这时候外部依赖只是“喂数据”的角色用 stub 最合适。比如计算运费依赖一个距离服务stub 返回固定距离然后断言运费金额。这里完全不需要关心距离服务被调了几次。第二类验证副作用。被测代码执行后必须对外部世界产生某种影响——发消息、写日志、调接口。这时候用 mock断言“这个影响确实发生了且参数正确”。注意只验证真正重要的副作用别把每个调用都当副作用。第三类验证完整流程。被测代码要和依赖来回交互多次读一点、写一点、再读回来。这种场景 stub 和 mock 都力不从心因为它们没有真实状态。用 fake让依赖真实运转起来测试才有意义。仓储层的读写测试、缓存命中测试都属于这一类。3.2 一个订单场景的完整选型演示拿一个下单流程举例把三类替身放在同一个场景里对比选型逻辑会更直观。假设下单流程是校验库存 → 扣减库存 → 生成订单 → 发送通知。四个依赖分别是库存服务、订单仓储、通知服务、还有一个汇率服务用于计算跨境订单金额。汇率服务只用来取一个汇率值参与计算用stub固定返回 7.2。库存服务扣减是必须发生的副作用用mock验证deduct被调用一次且参数正确。订单仓储需要真实存取测试里要能查回刚写入的订单用fake内存仓储。通知服务发通知是副作用但很多团队会选择用 mock 验证“发了一次”也有人用 fake 记录所有发出的通知再统一断言。两种都行取决于你想验证的粒度。这个例子里三种替身同时出现各司其职没有谁替代谁。这正是我想强调的它们本来就是配合使用的不是二选一。3.3 什么时候干脆别用替身有个反直觉的建议能用真实对象就别用替身。替身是有成本的——写它、维护它、还要保证它和真实实现行为一致。如果一个依赖足够轻量、足够快、足够确定比如一个纯内存的算法类、一个不涉及 IO 的工具类直接用真实对象测试反而更可信。判断标准可以简化成三条这个依赖慢不慢涉及网络、磁盘、稳不稳结果是否随机、是否依赖时间、贵不贵调用是否收费、是否有配额。三条里中了一条以上才考虑上替身。三条都不沾老老实实用真的。注意见过不少团队为了“测试纯粹”把连字符串工具类都要 mock 一遍结果测试代码比业务代码还长。这不是严谨是自找麻烦。4. 落地实操从零写出一套清晰的测试替身4.1 目录与命名约定先行概念清晰的第一步是命名清晰。我强烈建议在项目里给测试替身单独建一个包或目录并且用后缀明确类型XxxStub、XxxFakemock 因为是框架动态生成的通常不需要单独建类但变量名要体现用途比如stockServiceMock。命名约定看起来是小事实际收益很大。新人进项目看到InMemoryOrderRepositoryFake立刻知道这是个内存版仓储看到FixedRateStub知道它只提供固定值不用点进去猜。反过来如果全叫MockXxx读代码的人就得逐个看实现才能判断行为沟通成本陡增。目录结构上我习惯这样组织test/ doubles/ stub/ // 所有 stub 实现 fake/ // 所有 fake 实现 service/ // 业务测试 repository/ // 仓储测试这样一眼能看出项目里有哪些替身、各是什么类型复用也方便。4.2 手写一个内存仓储 Fake 的完整过程以订单仓储为例走一遍 fake 的完整实现。假设生产接口是这样public interface OrderRepository { void save(Order order); Order findById(String id); ListOrder findByStatus(String status); }内存 fake 的实现思路是用一个 Map 当存储key 是订单 IDpublic class InMemoryOrderRepositoryFake implements OrderRepository { private final MapString, Order store new HashMap(); Override public void save(Order order) { store.put(order.getId(), order); } Override public Order findById(String id) { return store.get(id); } Override public ListOrder findByStatus(String status) { return store.values().stream() .filter(o - status.equals(o.getStatus())) .collect(Collectors.toList()); } }这段代码不长但有几个细节值得说。第一findById找不到时返回null这必须和真实实现的行为一致——如果真实仓储找不到会抛异常fake 也得抛否则测试通过但线上炸。第二findByStatus用了流式过滤逻辑简单但真实可用这正是 fake 的价值所在。第三store用HashMap而非ConcurrentHashMap因为单元测试通常单线程没必要引入额外复杂度但如果测试涉及并发就得换。写完 fake 后建议给它本身写一两个测试验证“存进去能取出来”“按状态查得到”。这听起来有点绕——测试替身也要被测试是的因为 fake 有真实逻辑逻辑就可能出错。给 fake 写测试本质是保证这个“道具”可靠否则基于它的所有测试都不可信。4.3 Mock 的验证粒度怎么把握Mock 用得好不好全在验证粒度。粒度太粗测不出问题粒度太细测试脆弱。我的经验是抓住“业务上不可省略的交互”来验证。什么叫不可省略就是“如果这个调用没发生业务上就是错的”。下单不扣库存业务错下单不发通知业务可能允许通知可以异步补发那通知的调用就不必用 mock 严格验证用 fake 记录一下、做个宽松断言即可。具体到写法验证时优先用“至少一次”而非“恰好一次”除非业务真的要求幂等。times(1)写起来爽但一旦引入重试机制就全红。用atLeastOnce()表达“这个副作用必须发生”语义更稳也更贴近业务意图。另外验证参数时尽量只验证关键字段别把整个对象做相等比较。对象里加个时间戳字段相等比较就挂了但业务上时间戳根本不重要。只断言“订单 ID 和数量对得上”测试就稳得多。4.4 让替身可复用的三个小技巧第一个技巧是给 fake 加清空方法。测试之间共享 fake 实例时前一个测试的数据会污染后一个加个clear()在BeforeEach里调用问题就解决了。第二个技巧是stub 用构造参数而非硬编码。与其写死返回 7.2不如让构造函数接收汇率值同一个 stub 类就能复用于多个场景不用为每个值写一个新类。第三个技巧是mock 的验证逻辑抽成辅助方法。多个测试都要验证“库存扣减正确”就把这段 verify 抽成一个assertStockDeducted(mock, sku, qty)方法测试主体更清爽验证标准也统一。5. 踩坑实录与排查清单5.1 那些年我们写错的替身坑一stub 返回值不全导致空指针。被测代码里有个分支处理“汇率服务返回 null”的情况结果 stub 永远返回非 null这个分支从没被覆盖上线后真遇到 null 直接崩。教训是stub 要覆盖被测代码关心的所有输入形态包括异常和空值。坑二mock 验证了不该验证的东西。有个测试验证“日志服务被调用了两次”后来日志框架升级内部实现改成批量写调用次数变了测试红了但功能没问题。这类测试纯属自找麻烦日志这种非核心副作用根本不该用严格 mock 验证。坑三fake 和真实实现行为漂移。内存 fake 的findByStatus对大小写敏感真实数据库配置的是不敏感测试里查得到、线上一查就空。这种漂移极难发现因为测试永远是绿的。对策是给 fake 写契约测试或者定期用真实实现跑一遍同样的用例做对照。坑四一个测试里三种替身全上还互相纠缠。测试又长又难读出了问题不知道是哪部分导致的。对策是拆测试一个测试只验证一个关注点替身数量自然就降下来了。5.2 常见问题速查表现象可能原因排查方向测试永远是绿的但线上出 bug该验证的交互没验证stub 掩盖了问题检查关键副作用是否用了 mock 验证重构后大量测试变红功能没变mock 验证了实现细节减少 verify 断言只保留业务关键交互测试之间互相影响单独跑能过fake 状态未清理加 clear 方法并在前置钩子调用fake 测试通过但集成测试失败fake 与真实实现行为不一致补契约测试对照真实实现测试代码比业务代码还长替身过度使用评估依赖是否真的需要替身5.3 几条压箱底的经验第一条先写测试再决定替身。别一上来就想“我该用 mock 还是 stub”先把测试的断言写出来看断言需要什么替身类型自然就定了。断言是“结果等于 X”用 stub断言是“某方法被调用”用 mock断言是“数据能读回来”用 fake。第二条替身越少越好越简单越好。一个测试里替身超过三个就该反思是不是被测单元职责太重。替身数量往往是设计问题的信号。第三条定期清理不再使用的替身。项目演进中很多替身会变成孤儿没人用还占着维护成本。每隔一段时间扫一遍doubles目录删掉没人引用的保持整洁。第四条团队内统一术语。在代码规范或 README 里写清楚stub 指什么、mock 指什么、fake 指什么新人入职先读这段。术语统一了评审时的无效争论能少一大半。我在实际项目里推这套约定时最开始也有人觉得“太较真”但跑了两三个迭代后测试的稳定性明显提升重构时敢改代码了因为知道测试红了一定是功能真变了而不是替身写错了。这种“测试可信”的感觉是靠概念清晰一点点攒出来的不是靠某个框架或工具能直接给的。
企业数字化 ERP 产品动态
相关推荐
安卓语音助手语音链路实战:VAD断句、ASR/TTS与低延迟工程化 简介:这是一份面向安卓开发者的智能语音对话机器人工程源码包,适合想入门语音交互、自然语言处理或二次开发机器人助手的中级学习者参考。工程围绕语音指令采集与识别、文本语义理解、对话响应生成以及机器人大脑接口调用展开,可用于搭建具备… · 2026/11/5 16:40:02
HTML模块化开发:从原理到企业级实践 1. 模块化拆分的概念与价值模块化拆分是现代Web开发中至关重要的工程实践。简单来说,它就像把一本厚重的百科全书拆分成多个专题小册子——每个小册子独立成册,但又通过目录相互关联。在HTML开发中,这意味着将庞大的页面结构分解为可复用的组… · 2026/11/5 16:40:28
约瑟夫问题:经典算法解析与Python实现 1. 约瑟夫问题简介 约瑟夫问题(Josephus Problem)是一个经典的数学理论问题,源于公元1世纪犹太历史学家弗拉维奥约瑟夫斯的记载。问题描述如下:N个人围成一圈,从某个指定的人开始报数,数到第M个人就将其淘汰… · 2026/11/5 16:39:18
沈阳哈工特种机器人有效期多久 特种作业实操考试心里没底怕挂科?考前押题帮你稳过 实操考试心里没底怕挂科?你不是一个人。特种作业考试门槛高、内容多、实操要求严,很多人在报名后才发现,不是你不会,而是没掌握对方法。尤其是初次报考的安阳朋友,面对【沈阳哈工特种机器人】这类专业… · 2026/11/10 17:01:42
建筑业特种工查询官方回应:工地太忙根本没时间复习考试怎么办? 建筑业特种工查询官方回应:工地太忙根本没时间复习考试怎么办? 工地太忙根本没时间复习考试,这是大多数一线施工人员的真实写照。尤其是像河南房建工程这样工期紧、任务重的项目,特种作业操作证的考试和复审常常被忽视,导致很多人在岗位上被限制,甚至面… · 2026/11/10 17:01:42
电工证查询网东北上岗必须要这个证吗 东北电工证查询网多少钱?怕考不过白交培训费的看过来 你是不是也在犹豫要不要报电工证培训?怕考不过白交培训费,怕花了钱还得不到证?东北电工证查询网多少钱?其实这些问题,很多老电工都经历过。我认识一个从郑州转岗到东北的朋友,他就是因为没搞清楚培… · 2026/11/10 17:01:42
建筑电工证是属于哪个部门管理?良心建议来了 建筑电工证是属于哪个部门管理?良心建议来了 你是不是也遇到过这样的问题: 不知道去哪报名怕被中介坑 ?现在市面上太多培训机构打着“包过”“保证”“快速取证”的旗号,其实背后都是套路。今天我就从 漯河公路工程从业者 的角度出发,讲讲… · 2026/11/10 17:01:42
高低压合并的电工证到期换证流程怎么走?网上查询一步搞定 高低压合并的电工证到期换证流程怎么走?网上查询一步搞定 不知道去哪报名怕被中介坑?很多人在换证时都遇到过这样的情况,特别是对高低压合并的电工证这类专业证书,稍有不慎就可能被不良中介忽悠,浪费时间和金钱。其实,只要掌握正确的方法, 网上查询… · 2026/11/10 17:01:42
装修施工考什么电工证通过率揭秘:别让培训费打水漂 装修施工考什么电工证通过率揭秘:别让培训费打水漂 怕考不过白交培训费?装修施工考什么电工证,真的有那么难吗?其实,只要选对方向,准备充分,通过率并不低,关键看你是不是真的在“做题”而不是“瞎猜”。本文以漯河某项目现场管理员的实际经验为例,带… · 2026/11/10 17:00:42
电工证丢失还能补吗?千万别踩坑,丢了证等于失业 电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。… · 2026/11/10 2:16:10
电力电缆作业证直播报名入口与办理流程良心建议 电力电缆作业证直播报名入口与办理流程良心建议 你是不是也担心学历不高报不了名?别急,今天就给你讲讲 电力电缆作业证直播 的报名流程和那些你必须知道的干货, 良心建议 在这里,别错过。 电力电缆作业证直播报名需要什么条件?… · 2026/11/9 3:11:26
英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 怕考不过白交培训费?英德电工证报考费多少钱,你真的了解吗?现在市面上培训机构五花八门,价格参差不齐,很多人一不小心就花了冤枉钱,结果还考不过。别急,今天就来给你说清楚英德电工证的… · 2026/11/9 5:24:17
丹寨电工证怎么考?内部消息透露报考条件和流程 丹寨电工证怎么考?内部消息透露报考条件和流程 急需特种作业证才能上岗找工作,这是很多电工朋友最真实的焦虑。尤其是像丹寨电工证这样的证书,直接影响你的职业发展和薪资待遇。但你知道吗?很多人都在报考过程中踩坑,有的甚至花了几千块最后白忙一场。今… · 2026/11/10 0:00:13
成都建筑电工证办理郑州报考避坑指南 成都建筑电工证办理郑州报考避坑指南 证书过期了不知道怎么复审延期?别慌!这篇文章手把手带你解决成都建筑电工证办理的难题,顺便避掉郑州报考的那些坑。不管你是刚拿到证的新手,还是准备复审的老电工,这波干货都别错过。… · 2026/11/10 0:00:13
梅州低压电工证怎么考?怕被中介坑?手把手教你全国通用流程 梅州低压电工证怎么考?怕被中介坑?手把手教你全国通用流程 你是不是正为梅州低压电工证发愁?不知道去哪报名,怕被中介骗钱,又担心证书是不是全国通用?其实,这些顾虑都是正常人会有的。今天我就从一个在开封房建工程一线干了几年的电工角度,手把手教你… · 2026/11/10 0:00:13
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/9 11:23:35
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/9 11:23:58
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/10 17:00:47