>假设我有实现它的接口和实现类,我想为此编写单元测试。我应该测试什么接口或Impl?
下面是一个示例:
public interface HelloInterface {
public void sayHello();
}
public class HelloInterfaceImpl implements HelloInterface {
private PrintStream target = System.out;
@Override
public void sayHello() {
target.print("Hello World");
}
public void setTarget(PrintStream target){
this.target = target;
}
}
所以,我有HelloInterface和HelloInterfaceImpl来实现它。什么是被测单元接口或 Impl?
我认为应该是HelloInterface。考虑以下 JUnit 测试草图:
public class HelloInterfaceTest {
private HelloInterface hi;
@Before
public void setUp() {
hi = new HelloInterfaceImpl();
}
@Test
public void testDefaultBehaviourEndsNormally() {
hi.sayHello();
// no NullPointerException here
}
@Test
public void testCheckHelloWorld() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream target = new PrintStream(out);
PrivilegedAccessor.setValue(hi, "target", target);
//You can use ReflectionTestUtils in place of PrivilegedAccessor
//really it is DI
//((HelloInterfaceImpl)hi).setTarget(target);
hi.sayHello();
String result = out.toString();
assertEquals("Hello World", result);
}
}
主线实际上是我注释掉的一条。
((HelloInterfaceImpl)hi).setTarget(target);
方法 setTarget()
不是我的公共接口的一部分,所以我不想意外调用它。如果我真的想打电话,我应该花点时间考虑一下。例如,它帮助我发现我真正想做的是依赖注入。它为我打开了充满新机遇的整个世界。我可以使用一些现有的依赖注入机制(例如 Spring 的),我可以自己模拟它,就像我在代码中实际所做的那样,或者采取完全不同的方法。仔细看看,准备PrintSream并不是那么容易,也许我应该使用模拟对象?
编辑:我想我应该始终专注于界面。从我的角度来看setTarget()
也不是 impl 类"契约"的一部分,它为依赖注入提供了 sally 服务。我认为从测试的角度来看,Impl 类的任何公共方法都应该被视为私有的。不过,这并不意味着我忽略了实现细节。
另请参阅是否应使用单元测试的专用/受保护方法?
EDIT-2 在多个实现\多个接口的情况下,我会测试所有实现,但是当我在setUp()
方法中声明一个变量时,我肯定会使用接口。
实现是需要测试的单元。这当然是你正在实例化的内容,也是包含程序/业务逻辑的内容。
如果你有一个关键接口,并且你想确保每个实现都正确遵守它,那么你可以编写一个测试套件,专注于接口并要求传入一个实例(与任何实现类型无关)。
是的,使用 Mockito for PrintStream 可能更容易,可能并不总是可以避免像您在此特定示例中所做的那样使用模拟对象。
我会测试界面。
我认为错误在于以这样一种方式编写了实现,以至于它很难写入System.out;你没有办法用另一个PrintStream覆盖。 我会使用构造函数而不是二传手。 没有必要以这种方式进行模拟或选角。
这是一个简单的案例。 我想一个更复杂的工厂会有一个工厂来创建不同的、更复杂的接口实现。 希望你不会以这样一种方式设计它,你会被框住。
在测试中坚持使用界面也会使模拟变得容易得多。
public class HelloInterfaceImpl implements HelloInterface {
private PrintStream target;
public HelloInterfaceImpl() {
this(System.out);
}
public HelloInterfaceImpl(PrintStream ps) {
this.target = ps;
}
@Override
public void sayHello() {
target.print("Hello World");
}
}
这是测试:
public class HelloInterfaceTest {
@Test
public void testDefaultBehaviourEndsNormally() {
HelloInterface hi = new HelloInterfaceImpl();
hi.sayHello();
}
@Test
public void testCheckHelloWorld() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream target = new PrintStream(out);
HelloInterface hi = new HelloInterfaceImpl(target);
hi.sayHello();
String result = out.toString();
assertEquals("Hello World", result);
}
}
我总是测试实现 - 一个类可以实现多个接口,一个接口也可以由多个类实现 - 每个测试都应该涵盖。
在单元测试中调用 setter 的要求(将接口强制转换为实现):
((HelloInterfaceImpl)hi).setTarget(target);
表示您实际测试了实现。这不是合同的一部分,但这是使实现工作的重要部分,应该正确测试。
让我们以JDK为例。您有接口List
和两个实现:ArrayList
和 LinkedList
。此外,LinkedList
实现Deque
接口。如果你为List
接口编写测试,你会涵盖什么?数组还是链表?更重要的是,在LinkedList
的情况下,您会选择测试什么接口? Deque
还是List
?如您所见,当您测试实现时,您没有这样的问题。
就我个人而言,在单元测试中将接口转换为实现是出现问题的明显迹象;)
说这取决于实现以及它在接口合约之外的作用。 许多实现只实现了接口中提供的功能,在其他实现中,接口只是类功能的一小部分。 它可以实现多个接口。
最终,您正在测试实现。
在像你定义的简单案例中,我说一个中的六个和另一个中的六个。 把你的测试用例写到接口或者实现上,只要对实现进行了充分的测试,结果都是一样的。
举一个不同的例子,我将有一个类,通过装饰真实的读者和作者来收集通信渠道的统计信息。 我的类现在可以实现这些接口,但它也会收集统计信息,这与任何一个合约都无关。 我当然仍然可以基于这些接口编写测试,但它不会完全测试这个类。