被测单元:Impl 还是 Interface?

假设我有实现它的接口和实现类,我想为此编写单元测试。我应该测试接口或 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);

方法不是我的公共接口的一部分,所以我不想意外地调用它。如果我真的想打电话给它,我应该花点时间思考一下。例如,它帮助我发现我真正想做的是依赖注入。它为我打开了整个新机遇的世界。我可以使用一些现有的依赖注入机制(例如Spring的),我可以自己模拟它,就像我在代码中实际做的那样,或者采取完全不同的方法。仔细看看,PrintSream的准备工作并不那么容易,也许我应该使用模拟对象来代替?setTarget()

编辑:我认为我应该始终专注于界面。在我看来,它也不是 impl 类的“契约”的一部分,它为依赖注入提供了 sally。我认为从测试的角度来看,Impl类的任何公共方法都应该被视为私有方法。不过,这并不意味着我忽略了实现细节。setTarget()

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

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


答案 1

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

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

是的,将Mockito用于PrintStream可能会更容易,并且可能并不总是能够避免像您在此特定示例中所做的那样使用模拟对象。


答案 2

我会测试界面。

我认为错误在于以这样一种方式编写实现,即写入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);
    }
}

推荐