我同意 David Whiteman 的观点,即在其他开发人员的测试和您自己的测试之间,每个测试都可能覆盖对方的盲点。然而,这不仅仅关乎代码覆盖率,还关乎测试设计。
我在最近的两门“编程入门”类型课程中的单元测试经验表明,即使是重叠的测试也可能有价值——在针对自己的代码编写测试时,将测试结果与测试实现分离可能会更加困难。一门课程实际上鼓励将实现写入测试,当这种情况发生变化时,即使行为正确,也会发生级联故障。
为了澄清我的意思,常见的措辞是"test the interface, not the implementation." 在为您的代码设计测试时,您应该只依赖接口的契约代码应该提供给其他班级。例如,访问它正在测试的对象的成员变量的测试应该使用 getter 和 setter,或者不使用,因为变量可能被重命名甚至删除。如果有问题的成员变量是一个对象,它甚至可能会改变类型,更不用说改变其成员变量的类型了。
考虑针对以下类编写测试,为简单起见,它恰好是 HomemadeStack。
public class HomemadeStack {
private Object[] theStack;
@postcondition: stack is not empty, and contains one more element
public void push(Object o) {
// implementation specific to an array
}
@precondition: stack is not empty
@postcondition stack contains one less element
public Object pop() throws StackUnderflowException {
// implementation specific to an array
}
@postcondition: the stack is empty (isEmpty() will return true)
public void clear() {
// again, implementation specific to an array
}
@returns: true only if there are no objects on the stack
public boolean isEmpty() {
// again, implementation specific to an array
}
}
如果您必须更改访问修饰符来执行测试,至少根据我的经验,这是您测试错误的好兆头。 HomemadeStack 中的变量 theStack 应该 是私有的。前面提到的课程让我们编写如下测试,其中 testStack 在 HomemadeStack 的非空实例中:
void testClearMethodResultsInEmptyStack {
testStack.clear();
assert testStack.theStack.size()==0;
}
此测试对实现的假设过多。它还需要开发人员为 TheStack 提供更宽松的访问修饰符,以便从外部类进行测试。但是,像这样写一个测试很容易,然后写清楚的方法来满足测试。它通过了,所以有问题吗?好吧,列出上述测试中固有的一些实现假设:
- HomemadeStack 中存在一个名为 theStack 的变量
- 无论 theStack 是什么类型的变量,它都是一个带有 size() 方法的对象
- 最重要的是,“已清除”堆栈的底层数据结构大小为 0,而不是与清除前相同,除非更新了“第一个可用”指针或其他方法。李>
现在,假设您被告知将 HomemadeStack 转换为在内部使用链表,并将变量命名为“theList”。即使新的实现正确地推送、弹出、清除和检查空性(即实现接口的契约),测试中的任何实现假设都不会存在!所以现在你已经编写了一个测试失败的工作类,但没有人能说问题出在哪里。会有多少小时的惊愕和额外的工作?
为了在没有这些额外假设的情况下测试接口,我在另一门课程中被教导要依赖 isEmpty() 和 clear() 的契约:
void testClearMethodResultsInEmptyStack {
testStack.clear();
assert testStack.isEmpty();
}
还可以依靠 clear() 来测试 pop() 在其合约被破坏时抛出 StackUnderFlowException 等。这是对接口的测试,它更健壮。如果您从正在测试的代码中“退后一步”,那么编写“退后一步”的测试会更容易。
作为事后的想法,我越是反思,我就越相信那门教我以“错误的方式”处理测试驱动开发的编程课程正试图以艰难的方式教学生为什么存在最佳实践,并且在事情破裂之前陷入不太理想的做法时会产生非常糟糕的感觉。另一门课程教 TDD 有多少人会考虑“正确的方法”,但让我们的学生没有亲身体验为什么它是最好的。