
Apache Cassandra 测试指南单元测试、集成测试与 dtests 的编写规范与可测试性实践【免费下载链接】cassandraOpen source transactional distributed database. Linear scalability and proven fault-tolerance on commodity hardware or cloud infrastructure without compromising performance.项目地址: https://gitcode.com/GitHub_Trending/cassa/cassandra本指南以仓库根目录 TESTING.md 为骨架系统梳理 Apache Cassandra 项目对测试范围、测试风格与测试目标的要求涵盖单元测试Unit Tests、集成测试Integration Tests与 dtests 三类测试的职责边界以及全局状态处理、未测试代码重构等可测试性工程实践。读完本文你将掌握 Cassandra 提交贡献时的测试验收标准、分布式组件在 JUnit 中确定性测试的方法以及如何通过依赖注入或VisibleForTesting钩子把强耦合全局状态如FailureDetector.instance、CompactionManager.instance的代码改造为可穷举测试的形态。文档定位一份不设过多规则的测试增量改进指南TESTING.md 的目标是建立一套能推动 Cassandra 测试覆盖率与可测试性增量改进的编写指南同时不使日常开发负担过重。文档开篇即声明其立场并非每一条准则都能在每个贡献中立即落地也不是每条都与每次贡献都相关——它宁可偏向不为潜在例外立规则errs on the side of not making rules out of potential exceptions即允许合理例外但为测试的范围、风格、目标以及全局状态处理和未测试代码重构给出明确方向。其核心倡导可以概括为每次提交都应围绕你的 JIRA 主题补充测试测试债务的偿还幅度应与改动规模大致成比例——小 bug 修复只需恰好可测大改动则必须付出更多测试工作。这是一种务实的渐进式改善哲学而非一次性完美主义。三类测试的职责边界该测什么、不该测什么Cassandra 的测试体系主要由三类组成单元测试、集成测试和 dtests。每一类都有清晰的该测/不该测清单理解这一边界是正确编写测试的前提。单元测试Unit Tests窄范围的 JUnit 小部件测试单元测试是对较小组件如数据结构、verb handlers、辅助类的 JUnit 测试范围相当聚焦。应该测试的内容所有状态转换state transitions非法的状态转换应抛出异常所有条件分支涉及取值范围ranges of values的代码预期范围、非预期范围、不同功能区间及其边界值异常处理路径。不应该测试的内容实现细节——测试的是被测系统按某种方式工作而不是它按某种方式被实现。例如不应断言内部使用了某个具体集合类型或某种排序算法除非那是契约的一部分。在仓库中这类测试分布于 test/unit/org/apache/cassandra 之下与src/java/org/apache/cassandra的包结构一一对应。例如网络层有 MessageTest.java、ConnectionTest.java 等覆盖消息序列化与连接状态机的小部件测试故障检测器则有 FailureDetectorTest.java 对 Phi 值计算与存活判定做窄范围验证。集成测试Integration Tests多组件协作的 JUnit 测试集成测试是对含大量活动部件a lot of moving parts的较大组件的 JUnit 测试通常涉及节点间通信如 Gossip、MessagingService。这类测试中被测系统内部的小组件应当各自先有单元测试——集成测试不负责替代底层单元测试。应该测试的内容消息在预期时机被发送收到的消息产生预期的副作用内部接口按预期工作外部接口按预期被交互多个组件实例按预期互相协作在适当处使用 mocked 的 messaging service 及其它依赖dry start节点首次启动时系统能否正常启动restart节点重启后能否正常启动涵盖 clean 与 unclean shutdown 两种情形shutdown系统能否正常关闭upgrade系统能否携带上一版本的数据重启升级兼容。不应该测试的内容应用程序的其余部分——借助 mock应当可以在不启动整个数据库的情况下测试大型系统。文档特别提示一般不建议 mock 掉存储层。如果被测组件需要与存储层交互且你要测试多个实例之间的交互正确的做法是参数化parameterize它们存储数据的 keyspace/table而不是 mock 存储。这与 Cassandra 测试基建中的SchemaLoader见 test/unit/org/apache/cassandra/SchemaLoader.java和ServerTestUtils的思路一致测试通过真实的 CQL 建表/读写路径获得可复现的环境而非伪造存储行为。dtests黑盒集群测试dtests 是使用 Python 与 ccmCassandra Cluster Manager启动本地集群、并通过 Python 客户端与集群交互的测试。它们是事实上的黑盒测试用于验证集群与客户端侧接口是否按预期工作。文档强调dtests 不能替代真正的 Java 功能测试——它们运行时间长得多且可测试的内容灵活性差得多。因此dtests 中被测系统应当同时拥有更细粒度的集成测试。应该测试的内容端到端的集群功能客户端契约client contracts易于构造的失败场景trivial to create failure cases。不应该测试的内容内部实现细节。展开准则测试结构、分布式组件与分支/输入覆盖测试结构setup → 前置检查 → 动作 → 后置检查测试用例应当有清晰的四段式推进setup准备、前置条件检查precondition check、执行被测动作action under test、后置条件检查postcondition check。文档给出的规范示例Test public void increment() throws Exception { // setup code int x 1; // check preconditions assertEquals(1, x); // perform the state changing action under test x; // check post conditions assertEquals(2, x); }理由测试用例应以可读性为最优先优化目标。清晰的段落划分让失败的测试能立即定位到是前置假设错了、动作没生效还是副作用不符合预期。例外简单的单行用例可以豁免例如校验逻辑validation检查以及基于属性的状态测试如 ScalaCheck/QuickCheck 风格。文档给出的校验测试示例通过一个assertValidationFailure辅助方法把对 builder 各种非法字段的断言浓缩为一行一个用例Test public void validation() { assertValidationFailure(b - b.withState(null)); assertValidationFailure(b - b.withSessionID(null)); assertValidationFailure(b - b.withCoordinator(null)); assertValidationFailure(b - b.withTableIds(null)); assertValidationFailure(b - b.withTableIds(new HashSet())); assertValidationFailure(b - b.withRepairedAt(0)); assertValidationFailure(b - b.withRepairedAt(-1)); assertValidationFailure(b - b.withRanges(null)); assertValidationFailure(b - b.withRanges(new HashSet())); assertValidationFailure(b - b.withParticipants(null)); assertValidationFailure(b - b.withParticipants(new HashSet())); assertValidationFailure(b - b.withStartedAt(0)); assertValidationFailure(b - b.withLastUpdate(0)); }这种风格与 Cassandra 中 builder 模式的防御性编程相呼应非法参数null、空集合、0、负值应当被构造阶段直接拒绝而不是等到使用阶段才暴露。在 JUnit 中测试分布式组件依赖节点间通信的组件应当能在纯 JavaJUnit中完成测试而不是只能借助真实集群。理由分布式系统最难的部分之一是保证在各种失败模式下行为仍然正确——包括现实中极少出现的、节点间事件特定顺序排列的怪异边界情况。在 JUnit 中测试这些场景要容易得多mock clusters 可以被置于特定状态然后确定性地逐步推进一串事件序列确保每一步之后组件都处于预期状态。相比之下dtests 面对真实时序时既难以复现特定交错也无法精确断言中间态。例外这条规则主要适用于新系统或大幅重构过的系统。较老的系统理应被重构以得到充分测试但这不是接手维护它们的先决条件。仓库对这一理念的落地体现在 test/simulator确定性事件模拟器用于精确控制跨节点事件顺序与 test/distributed/org/apache/cassandra分布式测试套件等目录中它们共同构成不用启动真实集群也能验证分布式行为的测试基建。测试所有分支与输入方法的全部分支和输入都应被覆盖。对需要多个条件同时满足的分支如x 10 y 100应当既有所有条件满足时分支被走到的用例如x11, y99也有仅满足其一、分支不被走到的用例如x11, y200或x5, y99。如果方法处理取值范围如x 10必须测试范围边界如x9, x10。文档示例注意其分支结构aFlag aValue 10优先于aValue 5class SomeClass { public static int someFunction(bool aFlag, int aValue) { if (aFlag aValue 10) { return 20; } else if (aValue 5) { return 10; else { return 0; } } } class SomeTest { public void someFunction() throws Exception { assertEquals(10, somefunction(true, 11)); assertEquals(5, somefunction(false, 11)); assertEquals(5, somefunction(true, 8)); assertEquals(5, somefunction(false, 8)); assertEquals(0, somefunction(false, 4)); } }从源码结构看Cassandra 中大量涉及范围判断的逻辑如超时配置、配额、直方图估计等都遵循同一覆盖要求边界值两侧9与10各需一个用例组合条件flag 与 value 的组合的四种布尔象限都要出现避免只测了分支被命中而漏掉分支未被命中的路径。测试所有状态转换这是测试所有分支与输入在有状态系统上的延伸应有用例证明状态在预期情况下发生转换并且状态变化带来预期副作用。例如节点从 JOINING 到 NORMAL、从 NORMAL 到 LEAVING 的迁移以及迁移后对读写路径、流式传输路径的影响都应有显式断言。测试不支持的参数与状态抛出异常如果系统不打算在某种状态下执行某动作如 bootstrap 期间的节点执行读取或某方法不打算遇到某类参数如只接受正数的数值方法就应有用例证明此时抛出合适的异常——对应地是IllegalStateException或IllegalArgumentException。文档明确推荐使用Guava Preconditions 模块来让这种防御式检查变得直白。理由对方法和系统的无意误用往往会引发静默且隐蔽的 bug。在不被支持的用法上抛异常能防止未来引入缺陷并减少开发者的意外reduces developer surprise。这一点在仓库中有大量对应实现例如各种 Builder 在构造阶段即通过checkNotNull/checkArgument拒绝非法输入与前述 validation 测试示例一一对应失败检测器、流式传输管理器等组件在非法状态下调用时也会抛出明确异常而非静默忽略。应对全局状态可测试性改造的三种递进手段文档坦承项目中存在大量全局状态这让编写健壮测试变得困难——但并非不可能。依赖全局状态不是不测试某个东西、或丢一个 dtest / 断言草草了事的借口。让与全局状态交互的代码可确定性测试只需要几个小调整。反面示例直接访问全局单例以下示例中verb handler 直接读取全局状态FailureDetector.instance再对另一些全局状态StreamPlan、CompactionManager.instance采取动作。这些全局状态都难以在测试中操控因此全面测试几乎不可能写出。更糟的是FailureDetector、streaming、compaction 是否工作正常并非本测试的关注点——我们关心的只是SomeVerbHandler是否按预期工作。class SomeVerbHandler implements IVerbHandlerSomeMessage { public void doVerb(MessageInSomeMessage msg) { if (FailureDetector.instance.isAlive(msg.payload.otherNode)) { new StreamPlan(msg.payload.otherNode).requestRanges(someRanges).execute(); } else { CompactionManager.instance.submitBackground(msg.payload.cfs); } } }注文中IVerbHandler在仓库中的真实定义位于 src/java/org/apache/cassandra/net/IVerbHandler.java其方法签名为void doVerb(MessageT message) throws IOException所有具体 verb 处理器都通过MessagingService注册后由消息分发线程调用。文档示例为说明问题做了简化使用了MessageIn等旧式类型名实际语义一致。正面示例构造器注入接口理想情况下类根本不应依赖全局状态而是把工作所需的一切以构造器参数传入。这既能实现全面测试又能阻止全局状态的蔓延还能开始识别和定义将取代全局状态的内部接口。class SomeVerbHandler implements IVerbHandlerSomeMessage { private final IFailureDetector failureDetector; private final ICompactionManager compactionManager; private final IStreamManager streamManager; public SomeVerbHandler(IFailureDetector failureDetector, ICompactionManager compactionManager, IStreamManager streamManager) { this.failureDetector failureDetector; this.compactionManager compactionManager; this.streamManager streamManager; } public void doVerb(MessageInSomeMessage msg) { if (failureDetector.isAlive(msg.payload.otherNode)) { streamExecutor.submitPlan(new StreamPlan(msg.payload.otherNode).requestRanges(someRanges)); } else { compactionManager.submitBackground(msg.payload.cfs); } } }对应的测试通过手写仪表化Instrumented接口实现来替换真实依赖从而穷举所有路径class SomeVerbTest { class InstrumentedFailureDetector implements IFailureDetector { boolean alive false; Override public boolean isAlive(InetAddress address) { return alive; } } class InstrumentedCompactionManager implements ICompactionManager { boolean submitted false; Override public void submitBackground(ColumnFamilyStore cfs) { submitted true; } } class InstrumentedStreamManager implements IStreamManager { boolean submitted false; Override public void submitPlan(StreamPlan plan) { submitted true; } } Test public void liveNode() throws Exception { InstrumentedFailureDetector failureDetector new InstrumentedFailureDetector(); failureDetector.alive true; InstrumentedCompactionManager compactionManager new InstrumentedCompactionManager(); InstrumentedStreamManager streamManager new InstrumentedStreamManager(); SomeVerbHandler handler new SomeVerbHandler(failureDetector, compactionManager, streamManager); MessageInSomeMessage msg new MessageIn(...); assertFalse(streamManager.submitted); assertFalse(compactionManager.submitted); handler.doVerb(msg); assertTrue(streamManager.submitted); assertFalse(compactionManager.submitted); } Test public void deadNode() throws Exception { InstrumentedFailureDetector failureDetector new InstrumentedFailureDetector(); failureDetector.alive false; InstrumentedCompactionManager compactionManager new InstrumentedCompactionManager(); InstrumentedStreamManager streamManager new InstrumentedStreamManager(); SomeVerbHandler handler new SomeVerbHandler(failureDetector, compactionManager, streamManager); MessageInSomeMessage msg new MessageIn(...); assertFalse(streamManager.submitted); assertFalse(compactionManager.submitted); handler.doVerb(msg); assertFalse(streamManager.submitted); assertTrue(compactionManager.submitted); } }通过把对全局状态的访问抽象成接口可以穷举式地测试该 verb handler 的所有路径并直接确认它采取了正确动作。文档同时给出两点重要提醒这里使用的接口可能不应与 MBeans 所用的接口相同——测试友好接口与运维管理接口的关注点不同不应混为一谈对于更复杂的类/函数这种注入式测试让全面测试容易得多。这些接口在仓库中都有真实对应物IFailureDetector定义于 src/java/org/apache/cassandra/gms/IFailureDetector.java包含isAlive、interpret、report、remove、forceConviction及事件监听注册方法ICompactionManager定义于 src/java/org/apache/cassandra/db/compaction/ICompactionManager.java其生产实现是 CompactionManager.java流式传输侧的管理入口是 StreamManager.java。可以看到Cassandra 事实上已经沿着定义接口 → 注入依赖的方向持续演进。折中方案VisibleForTesting保护方法在某些情况下向构造器传接口并不现实启动时实例化的类需要小心处理因为访问单例可能改变数据库的初始化顺序而且对一次 bug 修复来说改动可能过大。此时把对全局状态的访问包进受保护方法protected methods并在测试中覆写它们可以达到同样效果class SomeVerbHandler implements IVerbHandlerSomeMessage { VisibleForTesting protected boolean isAlive(InetAddress addr) { return FailureDetector.instance.isAlive(msg.payload.otherNode); } VisibleForTesting protected void streamSomething(InetAddress to) { new StreamPlan(to).requestRanges(someRanges).execute(); } VisibleForTesting protected void compactSomething(ColumnFamilyStore cfs ) { CompactionManager.instance.submitBackground(); } public void doVerb(MessageInSomeMessage msg) { if (isAlive(msg.payload.otherNode)) { streamSomething(msg.payload.otherNode); } else { compactSomething(); } } }对应测试通过继承并覆写这些 protected 钩子记录是否被调用class SomeVerbTest { static class InstrumentedSomeVerbHandler extends SomeVerbHandler { public boolean alive false; public boolean streamCalled false; public boolean compactCalled false; Override protected boolean isAlive(InetAddress addr) { return alive; } Override protected void streamSomething(InetAddress to) { streamCalled true; } Override protected void compactSomething(ColumnFamilyStore cfs ) { compactCalled true; } } Test public void liveNode() throws Exception { InstrumentedSomeVerbHandler handler new InstrumentedSomeVerbHandler(); handler.alive true; MessageInSomeMessage msg new MessageIn(...); assertFalse(handler.streamCalled); assertFalse(handler.compactCalled); handler.doVerb(msg); assertTrue(handler.streamCalled); assertFalse(handler.compactCalled); } Test public void deadNode() throws Exception { InstrumentedSomeVerbHandler handler new InstrumentedSomeVerbHandler(); handler.alive false; MessageInSomeMessage msg new MessageIn(...); assertFalse(handler.streamCalled); assertFalse(handler.compactCalled); handler.doVerb(msg); assertFalse(handler.streamCalled); assertTrue(handler.compactCalled); } }这种方式保留了启动路径上的原有初始化顺序不触碰全局单例同时让测试能够零成本地注入行为——是改动最小化与可测试性之间的务实平衡点。仓库中VisibleForTesting注解被广泛使用正是这一准则在真实代码库中的常态化体现。重构已有代码与未测试代码的工程策略重构已有代码测试债务与改动规模成比例如果你在维护一段历史上测试覆盖不佳的代码为其编写测试会更困难因为代码当初可能就不是为测试而写的。你需要为你的 JIRA 主题补上测试这很可能伴随一些重构——但不必为了提交一个 bugfix 而完整重构一个巨大的类。基本原则你必须能在补丁前后验证你打算修改的行为偿还的测试债务量应与改动规模大致成比例小 bugfix只需重构到让修复对象可测为止即使开始触及测试实现细节的领域也可以接受——目标是增量改进而不是第一轮就做到完美更宏大的改动则需要额外工作来充分测试你的变更。重构未测试代码从小处着手、迭代外扩仓库中有若干组件几乎没有直接测试覆盖。文档鼓励对这些组件提升覆盖并特别指出对想参与项目的新人来说为这些组件补测试是熟悉代码库的绝佳途径。操作建议先就拟议重构的主题与范围征求反馈尤其是较大规模的重构——越小、越聚焦的重构越容易通过评审并被合入从较小的片段开始重构并测试它们然后迭代式地向外扩展最好分散在多个 JIRA中进行理想情况下每个补丁都应独立地为项目增加测试覆盖价值——重重构、轻测试的补丁不太可能被合入项目成员来来往往许多小型改进比几个未完成/进行中的重构对整个项目更有利。这与仓库的实际演进相互印证FailureDetectorTesttest/unit/org/apache/cassandra/gms/FailureDetectorTest.java、网络层的MessageTest、ConnectionTest等test/unit/org/apache/cassandra/net都是先有接口抽象、再有注入点、最后被全面覆盖这一路径的产物。仓库中的测试基建速览为了让上述准则可落地仓库提供了丰富的测试执行基础设施供查看与本地运行不作为修改对象单元/集成测试JUnit 用例集中于 test/unit/org/apache/cassandra2000 文件按src/java/org/apache/cassandra的包结构镜像组织分布式测试test/distributed/org/apache/cassandra 下为多节点分布式场景的 Java 测试确定性模拟器test/simulator 提供事件级确定性推进能力正是在 JUnit 中确定性测试分布式组件准则的实现载体HarnessHarrytest/harry 与 ci/harry_simulation.sh 用于长期随机化模拟验证微基准test/microbench 存放 JMH 微基准服务于性能相关改动dtestsPython/ccm 黑盒测试分散于仓库配套的 dtest 工程本仓库 pylib 提供 cqlsh 相关 Python 基础库。结语把可测试性当成持续工程TESTING.md 给出的不是一份僵硬的清单而是一套围绕增量改进、比例偿还、优先可读、穷举分支、隔离全局的工程哲学。无论你提交的是小 bugfix 还是大特性都可以遵循同一路线先让被测对象可测构造器注入接口或VisibleForTesting保护方法兜底再按setup → 前置检查 → 动作 → 后置检查的结构补齐该测的分支、状态转换与异常路径最后用与改动规模相称的重构量收尾——Cassandra 的测试覆盖率正是这样一步步被推高的。【免费下载链接】cassandraOpen source transactional distributed database. Linear scalability and proven fault-tolerance on commodity hardware or cloud infrastructure without compromising performance.项目地址: https://gitcode.com/GitHub_Trending/cassa/cassandra创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考