在设计可测试性时如何处理静态实用程序类


62

我们正在尝试将我们的系统设计为可测试的,并在大多数零件中使用TDD进行开发。当前,我们正在尝试解决以下问题:

在各个地方,我们有必要使用静态辅助方法,例如ImageIO和URLEncoder(均为标准Java API)以及各种其他主要由静态方法组成的库(例如Apache Commons库)。但是,测试使用此类静态帮助程序类的方法极其困难。

我有一些解决此问题的想法:

  1. 使用可以模拟静态类的模拟框架(例如PowerMock)。这可能是最简单的解决方案,但感觉有点像放弃。
  2. 围绕所有这些静态实用程序创建实例化包装器类,以便可以将它们注入使用它们的类中。这听起来像是一个相对干净的解决方案,但是我担心我们最终将创建大量这些包装器类。
  3. 将对这些静态帮助器类的每个调用提取到一个可以重写的函数中,并测试我实际要测试的类的子类。

但是我一直认为这只是许多人在进行TDD时必须面对的问题-因此必须已经有解决此问题的方法。

使使用这些静态帮助器的类可测试的最佳策略是什么?


我不确定“可信和/或官方消息”是什么意思,但我同意@berecursive在他的回答中写的内容。PowerMock的存在是有原因的,它不应该让人“放弃”,特别是如果您不想自己编写包装器类时。当涉及到单元测试(和TDD)时,最终方法和静态方法很麻烦。亲身?我使用您描述的方法2。
装饰

“悬赏和/或官方消息”只是启动悬赏时可以选择的选项之一。我的实际意思是:TDD专家撰写的文章的经验或参考。或遇到相同问题的人的任何经验……

Answers:


34

(恐怕这里没有“官方”资源-好像没有关于如何进行良好测试的规范。仅是我的观点,希望会有所帮助。)

当这些静态方法表示真正的依赖关系时,请创建包装器。所以对于这样的事情:

  • 图像IO
  • HTTP客户端(或与网络相关的任何其他客户端)
  • 文件系统
  • 获取当前时间(我最喜欢的依赖注入帮助的示例)

...创建一个界面很有意义。

但是,Apache Commons中的许多方法可能都不应该被模拟/伪造。例如,采用一种方法将字符串列表连接在一起,并在字符串之间添加逗号。有没有点在嘲讽这些-只是让静态调用完成其正常的工作。您不想或不需要替换常规行为;您无需处理外部资源或难以使用的东西,而只是数据。其结果是可以预见的,而你却不希望它是任何其他比它会给你的好意。

我怀疑删除了所有静态调用,它们实际上是具有可预测的“纯”结果(例如base64或URL编码)的便捷方法,而不是进入大量逻辑依赖项(例如HTTP)的入口点,您会发现它完全是用真正的依赖关系做正确的事是实际的。


20

这绝对是一个自以为是的问题/答案,但是我认为我应该花两美分。对于TDD风格方法2而言,绝对是紧随其后的方法。方法2的论据是,如果您想替换那些类之一的实现(例如一个ImageIO等效的库),那么您可以在保持对利用该代码的类的信心的同时做到这一点。

但是,就像您提到的那样,如果使用大量静态方法,那么最终将编写大量包装器代码。从长远来看,这可能不是一件坏事。在可维护性方面,对此肯定有论点。我个人更喜欢这种方法。

话虽如此,PowerMock的存在是有原因的。众所周知,当涉及到静态方法时进行测试非常痛苦,因此PowerMock的诞生。我认为您需要根据包装所有帮助程序类和使用PowerMock进行多少工作来权衡选择。我不认为使用PowerMock会“放弃”-我只是觉得包装类可以使您在大型项目中拥有更大的灵活性。您可以提供的公共合同(接口)越多,意图和实现之间的分隔就越清晰。


1
我不太确定的另一个问题:实现包装器时,您将实现包装类的所有方法还是仅实现当前需要的方法?

3
在遵循敏捷思想时,您应该做最简单的事情,避免做不需要的工作。因此,您应该只公开实际需要的方法。
阿萨夫·斯通

@AssafStone同意

使用PowerMock时要小心,模拟方法所必须进行的所有类操作都会产生大量开销。如果您广泛使用它,测试会变得很慢。
bcarlso'5

如果将测试/迁移与采用DI / IoC库相结合,您真的需要做很多包装工作吗?

4

作为所有也处理此问题并遇到此问题的人的参考,我将描述我们如何决定解决该问题:

我们基本上遵循概述为#2(静态实用程序的包装器类)的路径。但是,仅在过于复杂而无法向实用程序提供所需数据以产生所需输出时(即,当我们绝对必须模拟该方法时),才使用它们。

这意味着我们不必为诸如Apache Commons之类的简单实用程序编写包装器StringEscapeUtils(因为可以轻松提供它们所需的字符串),而且我们也没有将模拟方法用于静态方法(如果我们认为可能需要编写代码的话)包装器类,然后模拟包装器的实例)。



1

我在一家大型保险公司工作,我们的源代码高达400MB的纯Java文件。我们一直在开发整个应用程序,而没有考虑TDD。从今年1月开始,我们开始对每个组件进行junit测试。

我们部门中最好的解决方案是在系统依赖的某些JNI方法上使用Mock对象(用C编写),因此,您无法每次都在每个操作系统上准确地估算结果。除了使用模拟类和JNI方法的特定实现外,我们别无选择,这是为了针对我们支持的每个OS测试应用程序的每个单独模块。

但这确实非常快,并且到目前为止效果很好。我推荐-http: //www.easymock.org/


1

当您由于环境(Web服务端点,访问DB的dao层,处理HTTP请求参数的控制器)而使对象难以测试时,或者您想单独测试对象时,对象会相互交互以实现目标。你嘲笑那些对象。

模拟静态方法的必要性是一种难闻的气味,您必须将应用程序设计为更多面向对象的方法,并且单元测试实用程序的静态方法不会为项目增加太多价值,根据情况,包装器类是一种不错的方法,但是请尝试测试那些使用静态方法的对象。


1

有时我使用选项4

  1. 使用策略模式。用静态方法创建一个实用程序类,该方法将实现委派给可插拔接口的实例。编写一个插入具体实现的静态初始化程序。插入模拟实现进行测试。

这样的事情。

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

我喜欢这种方法的是,它使实用程序方法保持静态,当我尝试在整个代码中使用该类时,这种感觉对我来说是正确的。

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.