33

假设我有实现它的接口和实现类,并且我想为此编写单元测试。我应该测试什么接口或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 类的“合同”的一部分,它为依赖注入服务。我认为从测试的角度来看,任何 Impl 类的公共方法都应该被认为是私有的。不过,这并不意味着我忽略了实现细节。

另请参阅私有/受保护的方法是否应进行单元测试?

EDIT-2在多个实现\多个接口的情况下,我会测试所有的实现,但是当我在我的setUp()方法中声明一个变量时,我肯定会使用接口。

4

4 回答 4

19

实现是需要测试的单元。这当然是您要实例化的内容以及包含程序/业务逻辑的内容。

如果您有一个关键接口并且您想确保每个实现都正确地遵守它,那么您可以编写一个专注于接口并要求传入实例的测试套件(与任何实现类型无关)。

是的,将 Mockito 用于 PrintStream 可能会更容易,但可能并不总是可以避免像在这个特定示例中那样使用模拟对象。

于 2012-06-07T18:42:11.377 回答
8

我会测试界面。

我认为错误在于编写实现的方式是硬连线写入 System.out;你让自己无法用另一个 PrintStream 覆盖。我会使用构造函数而不是 setter。没有必要以这种方式模拟或铸造。

这是一个简单的案例。我想一个更复杂的会有一个工厂来创建不同的、更复杂的接口实现。希望你不会以这样的方式设计它,你会被装箱。

在您的测试中坚持使用界面也会使模拟变得更加容易。

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);
    }
}
于 2012-06-07T18:41:11.957 回答
7

我总是测试实现——一个类可以实现多个接口,一个接口可以由多个类实现——每个类都应该被测试覆盖。

在单元测试中调用setter的要求(将接口转换为实现):

((HelloInterfaceImpl)hi).setTarget(target);

意味着您实际测试了实现。这不是合同的一部分,但这是使实施工作的重要部分,应该进行适当的测试。

让我们以JDK为例。您有接口List和两个实现:ArrayListLinkedList. 另外LinkedList实现Deque接口。如果您为List接口编写测试,您会涵盖什么?数组还是链表?更重要的是LinkedList,您会选择测试什么接口?Deque还是List?如您所见,当您测试实现时,您不会遇到此类问题。

就我个人而言,在单元测试中将接口转换为实现显然是出现问题的迹象;)

于 2012-06-07T19:59:21.813 回答
1

我会说这取决于实现以及它在接口合同之外的作用。许多实现只实现接口中提供的功能,而在其他实现中,接口只是类功能的一小部分。它可以实现多个接口。

最终,您正在测试实现。

在您定义的简单案例中,我说六个中的一个和六个另一个。将您的测试用例写入接口或实现,只要它充分测试实现,结果是相同的。

举一个不同的例子,我会有一个类,通过装饰真实的读者和作者来收集通信渠道的统计信息。我的类现在可能实现了这些接口,但它也收集统计信息,这与任一合同无关。我当然仍然可以基于这些接口编写测试,但它不会完全测试这个类。

于 2012-06-07T19:50:36.007 回答