我正在开发一个框架来简化控制台应用程序的创建。我的框架是用Scala编写的,我正在使用ScalaTest和Mockito进行单元测试。
我需要能够嘲笑java.io.Console
,但它被宣布为最终的。我正在尝试实现 100% 的单元测试覆盖率,这是目前唯一阻碍我的事情 - 在功能和单元测试中。
到目前为止,我还没有能够用任何解决方案走得很远,我只是想不出一种方法来做到这一点。它没有实现我可以模拟的接口,该方法在其他任何地方都不可用,显然我无法扩展它。我在想也许有一种解决方案可能涉及某种动态方法来调用 readLine
和 readPassword
等方法,但我的经验也不够,无法通过这种思路进行任何思考!
你应该创建自己的接口来包装所有与java.io.Console
的交互,例如
public interface ConsoleService {
...
}
只要您仅通过 ConsoleService
的实例与控制台交互,那么您就可以模拟ConsoleService
并像往常一样测试 99% 的代码。ConsoleService
接口成为应用程序的边界,用于整个应用的功能测试以及直接与之交互的类的单元测试。
现在我们已经将问题的范围缩小到"如何测试ConsoleService
实现",我们需要有点创意。例如,可以将控制台输出重定向到文件并检查文件的内容。您甚至可能不想在 Scala 中测试ConsoleService
;您可以使用ConsoleService
编写一个框架应用程序,然后使用您选择的脚本语言在您喜欢的操作系统上启动真正的控制台,与您的框架应用程序交互并以这种方式测试ConsoleService
。您可以在这里获得您喜欢的创意(和黑客),因为:
- 它只影响少量的测试;和
- 您的应用程序可能会成熟到
ConsoleService
实现不需要太多更改的程度,即您古怪的测试解决方案不会给未来的开发人员带来很大的负担。
由于这些原因,很明显,保持ConsoleService
包装器非常薄是一个好主意,因为其中的任何逻辑都将通过奇怪的ConsoleService
测试进行测试,而不是友好的Scala测试。通常直接委派给java.io.Console
方法就足够了,但你应该允许应用程序的功能测试来驱除ConsoleService
接口,而不是做出任何假设(你的功能测试断言可能依赖于与模拟ConsoleService
的特定交互,或者可能依赖于存根的状态,测试ConsoleService
的测试实现,你可以在测试中控制)。
最后,您可能会认为 ConsoleService
包装器太薄,以至于它的实现确实需要任何单元/功能测试。ConsoleService
的实现可能对您的应用程序至关重要,以至于任何缺陷都将通过集成测试或在 UAT 中手动检查应用程序来暴露。
你最终可能会得到这样的东西(抱歉,我不会说Scala,所以它是Java):
public class RealConsoleService implements ConsoleService {
private final java.io.Console delegate;
public RealConsoleService(java.io.Console delegate) {
this.delegate = delegate;
}
@Override
public String readLine() throws IOError {
return delegate.readLine();
}
}
两个有趣的观点:
- 这是一个很好的例子,说明为什么测试驱动开发有助于编写灵活的代码。如果要使用另一种输入和输出方法重写框架,只需将
ConsoleService
重命名为更抽象的ApplicationInputOutputService
并插入不同的实现即可。 - 相同的概念可用于测试使用其他难以测试的 API 的应用程序。Java的许多有用的文件IO方法是静态方法,因此在测试中难以控制。通过包装上述接口,您的应用程序功能变得易于测试。