Java单元测试 - 适当的隔离?

时间:2011-08-30 02:30:30

标签: java unit-testing junit

我想知道我的单元测试方法是否错误:

我的应用程序有一个引导程序进程,初始化几个组件并为各种子系统提供服务 - 让我们称之为“控制器”。

在许多情况下,为了对这些子系统进行单元测试,我需要访问控制器,因为这些子系统可能依赖于它。我进行单元测试的方法是初始化系统,然后将控制器提供给需要它的任何单元测试。我通过继承来实现这一点:我有一个初始化和测试控制器的基本单元测试,然后任何需要控制器的单元测试都会扩展这个基类,因此可以访问它。

我的问题是:

(1)这是否实现了适当的隔离?我有理由认为单元测试应该是孤立完成的,这样它们才是可重复和独立的 - 我可以提供一个真正的初始化控制器,而不是嘲笑它或者试图模拟每个测试所需的特定环境吗?

(2)作为最佳实践(假设我之前的方法没问题) - 我应该为每个单元测试反复创建控制器,还是只需创建一次(其状态不会改变)。

3 个答案:

答案 0 :(得分:7)

如果我们提供一个“真正的”控制器来测试另一个组件,那么严格来说我们正在执行集成测试而不是单元测试。这不一定是坏事,但请考虑以下几点:

创建控制器的成本

如果控制器是一个重量级的物体,构造成本相当高,那么每个单元测试都会产生这个成本。随着单元测试数量的增加,该成本可能开始占据整个测试执行时间。始终希望保持单元测试的运行时尽可能小,以便在代码更改后快速周转。

控制器依赖

如果控制器是一个复杂的对象,它可能具有自己的依赖关系,需要实例化以构建控制器本身。例如,它可能需要访问某种数据库或配置文件。现在,不仅需要初始化控制器,还需要初始化控制器。随着应用程序随着时间的推移而发展,控制器可能需要越来越多的依赖关系,只是随着时间的推移使这个问题变得更糟。

控制器状态

如果控制器带有任何状态,单元测试的执行可能会改变该状态。反过来,这可能会改变后续单元测试的行为。这些变化可能导致单元测试的明显不确定性行为,引入掩盖错误的可能性。解决这个问题的方法是为每次测试重新创建控制器,如果创建成本很高(如上所述),这可能是不切实际的。

组合问题

被测单元和控制器对象的复合系统的可能输入组合的数量可能远大于单元的组合数量。这个数字可能太大而无法进行实际测试。通过使用存根或模拟对象代替控制器单独测试单元,可以更容易地控制组合的数量。

上帝对象

如果每个单元测试中的所有组件都可以方便地访问控制器,那么很有可能将控制器变为God Object,它知道系统中每个组件的所有内容。更糟糕的是,这些组件可能开始通过该上帝对象彼此交互。最终结果是应用程序组件之间的分离开始腐蚀,系统开始变得单一。

技术债务

即使控制器是无状态且今天实例化的便宜,也可能随着应用程序的发展而改变。如果在我们编写了大量单元测试后的那一天到来,我们可能会面临所有这些测试的大量重构练习。此外,实际的系统代码可能还需要重构以用较轻的接口替换所有控制器引用。存在重构成本显着的风险 - 可能甚至太高而无法考虑,导致系统“陷入”不良形式。

<强>建议

为了避免现在和将来出现这些陷阱,我的建议是避免将真正的控制器提供给单元测试。

完整的控制器可能难以有效地存根或模拟。这将导致(期望的)压力以将组件的依赖性表达为“薄的”聚焦界面,代替控制器可能呈现的“厚”,“厨房水槽”界面。为什么这是可取的?这是可取的,因为这种做法可以更好地分离系统组件之间的关注点,从而产生远远超出单元测试代码库的架构优势。

对于很多关于如何实现关注点分离以及通常编写可测试代码的良好实用建议,请参阅Misko Hevery的guidetalks

答案 1 :(得分:1)

  1. 我认为提供一个真正的控制器是可以的。这将为您的系统提供良好的集成测试。在我的公司,我们做了很多你正在做的事情:一个用于设置环境的基础测试类和继承它的实际测试用例。

  2. Hrm ......我想我可能会创建一次。那也将测试你的控制器,并确保它的状态不会改变,并且可以承受重复的调用。

答案 2 :(得分:1)

如果您正在寻找严格的单元测试,为什么不使用模拟对象,如EasyMock:

http://www.easymock.org/

这样,您可以为控制器提供“模拟”行为,而无需实例化它。 Unitils还提供与EasyMock的集成,这样,如果扩展UnitilsJUnit4单元测试类,您将获得自动模拟对象创建和注入。 Unitils还提供数据库单元/集成测试,但这对您的软件来说可能过度。