Java中测试依赖其他比较器的比较器的最佳方案是什么?
嘿,这个问题问到点子上了——TDD里处理有依赖关系的组件,尤其是这种嵌套比较器的场景,核心就是把依赖解耦,用Mock来隔离测试目标。我来一步步给你捋清楚怎么做:
第一步:先给父比较器做个小重构(为了可测试)
首先,你得把父比较器里硬编码的子比较器依赖改成可注入的——毕竟如果父比较器自己new子比较器,你根本没法替换成Mock。比如原来的代码可能是这样:
public class SomeParentComparator implements Comparator<SomeType> { // 硬编码依赖,没法替换 private final SomeChildComparator childComparator = new SomeChildComparator(); @Override public int compare(SomeType a, SomeType b) { int childResult = childComparator.compare(a, b); if (childResult != 0) { return childResult; } // 父比较器自己的排序逻辑 return a.getSomeField().compareTo(b.getSomeField()); } }
改成带构造参数的版本,同时保留生产环境用的无参构造:
public class SomeParentComparator implements Comparator<SomeType> { private final Comparator<SomeType> childComparator; // 测试用:允许注入任意Comparator(包括Mock) public SomeParentComparator(Comparator<SomeType> childComparator) { this.childComparator = childComparator; } // 生产用:默认用真实的子比较器 public SomeParentComparator() { this(new SomeChildComparator()); } @Override public int compare(SomeType a, SomeType b) { int childResult = childComparator.compare(a, b); if (childResult != 0) { return childResult; } return a.getSomeField().compareTo(b.getSomeField()); } }
这一步完全符合TDD的思路:因为你写测试时发现代码不可测,所以先重构让它具备可测试性。
第二步:用Mock隔离子比较器,写针对性测试
接下来就可以用Mock框架(比如Mockito,Java生态里最常用的)来模拟子比较器的行为,只测试父比较器的逻辑。这里分两种核心场景来写测试,严格遵守你提到的“每次只一个失败测试”的TDD原则:
场景1:子比较器返回非0结果时,父比较器直接转发结果
先写这个测试,此时重构后的代码已经能支持注入Mock,测试会失败(因为你还没确保父比较器正确转发),然后调整代码让测试通过:
import org.junit.jupiter.api.Test; import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; public class SomeParentComparatorTest { @Test void compare_whenChildReturnsNonZero_returnsChildResult() { // 1. 创建Mock的子比较器 Comparator<SomeType> mockChild = Mockito.mock(Comparator.class); SomeType a = new SomeType(); SomeType b = new SomeType(); // 2. 指定Mock的行为:比较a和b时返回1 when(mockChild.compare(a, b)).thenReturn(1); // 3. 把Mock注入父比较器 SomeParentComparator parent = new SomeParentComparator(mockChild); // 4. 执行测试并断言结果 int result = parent.compare(a, b); assertEquals(1, result); // 可选但推荐:验证Mock确实被调用了一次,确保逻辑走了依赖路径 Mockito.verify(mockChild).compare(a, b); } }
场景2:子比较器返回0时,父比较器执行自己的排序逻辑
写完第一个测试并让它通过后,再写这个测试,确保父比较器在子比较器判断两个对象相等时,会用自己的逻辑排序:
@Test void compare_whenChildReturnsZero_executesOwnLogic() { // 1. Mock子比较器,让它返回0(表示两个对象在子比较器眼里相等) Comparator<SomeType> mockChild = Mockito.mock(Comparator.class); SomeType a = new SomeType("apple"); // 假设SomeType有个someField字段 SomeType b = new SomeType("banana"); when(mockChild.compare(a, b)).thenReturn(0); // 2. 注入Mock到父比较器 SomeParentComparator parent = new SomeParentComparator(mockChild); // 3. 执行测试:此时父比较器应该用someField排序,"apple" < "banana"所以返回-1 int result = parent.compare(a, b); assertEquals(-1, result); // 验证Mock被调用过 Mockito.verify(mockChild).compare(a, b); }
几个关键注意点
- 关于列表排序的测试:不用单独测试
Collections.sort的逻辑(那是JDK的责任),只要确保父比较器的compare方法逻辑正确,排序自然会符合预期。如果一定要测排序,也是用注入了Mock的父比较器来排序,验证列表顺序是否符合你的预期。 - 严格遵守TDD原则:每次只写一个失败的测试,比如先写场景1的测试,让它失败,然后修复代码;再写场景2的测试,依此类推,避免多个失败测试带来的混淆。
- Mock的作用:这里Mock子比较器的核心是隔离父比较器的测试——我们不需要关心子比较器的逻辑是否正确(那是子比较器自己的测试用例要覆盖的),只需要确保父比较器正确依赖子比较器的结果即可。
内容的提问来源于stack exchange,提问作者Panh
相关产品推荐
相关产品推荐

