如果不是很痛苦,请试想一下您是2002年初在一家初创公司中担任开发人员的。充裕的现金,您和您的团队已受命使用最新和最好的Java™API构建大型数据驱动的Web应用程序。 您和您的管理层坚信最终将被称为敏捷过程 。 从第一天起,就已经使用JUnit编写了测试,并且作为Ant构建过程的一部分,该测试应尽可能频繁地运行。 最终,将设置cron作业以每晚运行一次构建。 稍后,有人会下载CruiseControl,并且不断增长的测试套件将在每次入住时运行。
现在快进到今天。
在过去的几年中,您的公司使用同样庞大的JUnit测试套件开发了庞大的代码库。 直到大约一年前,一切都还不错,那时您的测试套件超过了2,000个测试,人们开始注意到构建过程要花费三个多小时才能运行。 在此之前的几个月,由于CI服务器承受的压力,您在代码检入期间停止了通过持续集成(CI)运行单元测试。 您切换到每晚运行测试,第二天早上,开发人员试图找出失败的原因以及原因,这使他们压力很大。
这些天来,似乎该测试套件很少在晚上运行一次以上-为什么要这样做? 他们永远! 没有人有几个小时的等待时间,只是发现一切运行良好(或不正常)。 此外,整个测试套件都在夜间运行,对吗?
因为您很少运行测试,所以它们经常充满错误。 因此,您和您的团队已经开始质疑单元测试的价值:如果它们对代码质量如此重要,为什么会如此痛苦? 大家都同意,如果您只能以更敏捷的方式运行它们,则单元测试具有根本的价值。
尝试测试分类
您需要的是一种将构建过渡到更敏捷状态的策略。 您需要一个解决方案,该解决方案允许您每天多次运行测试,并将测试套件放回构建花费三个小时才能完成的状态。
在尝试提出使整个测试套件恢复正常状态的策略之前,可能需要重新考虑通用术语“单元测试”。 正如“我在家有动物”和“我喜欢汽车”这样的陈述不是很具体,不幸的是,短语“我们编写单元测试”也是如此。 如今,单元测试可能意味着任何事情。
以前面关于动物和汽车的陈述为例:它们导致更多的问题。 例如,您家里有哪种动物? 是猫,蜥蜴还是熊? “我在家有只熊”肯定不同于“我在家有只猫”。 同样,在与汽车销售员交谈时,“我喜欢汽车”并不是很有用。 您喜欢哪种类型的汽车:跑车,卡车或旅行车? 任何答案都可能导致您走上不同的道路。
同样,对于开发人员测试,按类型对测试进行分类可能会有所帮助。 这样做可以使语言更加精确,并使您的团队能够以不同的频率运行不同的测试类型。 分类是避免运行所有“单元测试”的可怕的三小时构建的关键。
三类
可视化您的测试套件,分为三层,每一层代表不同类型的开发人员测试,该测试由运行时间定义。 正如您在图1中看到的那样,每一层都增加了总构建时间的长度,无论是运行还是事实证明,都是如此。
图1.三层测试分类
![三层测试分类](https://i-blog.csdnimg.cn/blog_migrate/b34cffc8a3ed4d8cc50c8f3dc83975c7.png)
底层由运行最短的测试组成,您可以想象,这是最容易编写的。 他们也接触最少的代码。 顶层由更多高级测试组成,这些高级测试覆盖了应用程序的较大部分。 这些测试的编码难度更高,执行时间也更长。 中间层用于介于两个极端之间的测试。
这三个类别如下:
- 单元测试
- 组件测试
- 系统测试
让我们分别考虑每个。
1.单元测试
单元测试隔离地验证一个或多个对象。 单元测试不涉及数据库,文件系统或其他任何可能使测试运行时间更长的东西。 因此,可以从第一天开始编写单元测试。 实际上,这正是JUnit设计的目的。 单元测试的孤立概念位于无数模拟对象库的背后,该库有助于将特定对象与其外部依赖项隔离开来。 而且,可以在编写实际的被测代码之前就编写单元测试-因此是测试优先开发的概念。
单元测试通常很容易编写代码,因为它们不依赖于体系结构依赖性,而且运行速度也很快。 不利的一面是,单个单元测试提供的代码覆盖范围有限。 单元测试的价值(相当可观)是,它们使开发人员能够在尽可能低的级别上保证对象的可靠性。
因为单元测试运行如此之快且易于编写,所以代码库中应包含大量的单元测试,并且它们应尽可能频繁地运行。 无论在您的机器上还是在CI环境中,执行构建时都应始终运行它们(这意味着您应在将代码检入SCM系统时随时运行它们)。
2.组件测试
组件测试验证了多个对象一起交互,但是它们违反了隔离的概念。 因为组件测试涉及体系结构的多个层,所以它们通常处理数据库,文件系统,网络元素等。而且,组件测试在早期很难编写,因此将它们合并到真正的“测试优先/测试驱动”场景中充满挑战。
组件测试需要花费更长的时间来编写,因为它们比单元测试更复杂。 另一方面,由于覆盖范围广,因此与单元测试相比,它们实现的代码覆盖率更高。 他们还需要更长的时间来运行,所以跑了很多他们在一起可以大大提高你的整体测试时间。
许多框架简化了与测试大型体系结构组件相关的挑战。 DbUnit是此类框架的完美示例。 DbUnit通过处理在测试状态之间植入数据库的相关复杂性,使编写依赖数据库的测试变得更加容易。
当构建的测试时间延长时,通常可以预测会涉及到大量的组件测试。 因为这些测试要比真正的单元测试花费更长的时间,所以您可能会发现不能一直运行它们。 因此,在CI环境中,至少每小时运行一次测试是有意义的。 您也应该在签入任何代码之前始终在本地开发人员的机器上运行这些测试。
3.系统测试
系统测试可端对端地验证软件应用程序。 因此,它们带来了高度的体系结构复杂性:必须运行整个应用程序才能进行系统测试。 如果是Web应用程序,则需要访问数据库以及Web服务器,容器和任何相关的配置方面,才能运行系统测试。 因此,大多数系统测试是在软件生命周期的后一个周期中编写的。
系统测试对作者来说具有挑战性,并且要花费大量时间才能真正执行。 另一方面,就体系结构代码覆盖率而言,它们可以说是物有所值。
系统测试与功能测试非常相似。 所不同的是,他们不模仿用户,他们模仿一个。 与组件测试一样,已经创建了许多框架来帮助简化这些测试。 例如,jWebUnit通过模仿浏览器来简化Web应用程序的测试。
实施测试分类
因此,事实证明,您的单元测试套件实际上是一组单元测试,组件测试和系统测试。 此外,您现在在检查了测试之后便知道构建需要三个小时的原因是其中大多数是组件测试。 您的下一个问题是,如何使用JUnit实施测试分类?
您有几种选择,但让我们坚持两个最简单的选择:
- 创建与所需类别相对应的定制JUnit套件文件。
- 为每种测试类型创建自定义目录。
创建自定义套件
您可以使用JUnit的TestSuite
类(类型为Test
)将一组测试定义为一起。 首先创建一个TestSuite
实例,并向其添加相应的测试类或测试方法。 然后,您可以通过定义一个名为suite()
的public static
方法,将JUnit指向TestSuite
实例。 然后,包含的所有测试将一次运行。 因此,您可以通过创建单元TestSuite
,组件TestSuite
和系统TestSuite
来实现测试分类。
例如,清单1中显示的类创建一个TestSuite
将所有组件测试保存在suite()
方法中。 注意,该类不是非常特定于JUnit的。 它没有扩展TestCase
,也没有定义任何测试用例。 但是JUnit会反省地找到suite()
方法并运行它返回的所有测试。
清单1.用于组件测试的TestSuite
package test.org.acme.widget;
import junit.framework.Test;
import junit.framework.TestSuite;
import test.org.acme.widget.*;
public class ComponentTestSuite {
public static void main(String[] args) {
junit.textui.TestRunner.run(ComponentTestSuite.suite());
}
public static Test suite(){
TestSuite suite = new TestSuite();
suite.addTestSuite(DefaultSpringWidgetDAOImplTest.class);
suite.addTestSuite(WidgetDAOImplLoadTest.class);
...
suite.addTestSuite(WidgetReportTest.class);
return suite;
}
}
定义TestSuite
的过程确实需要您遍历现有测试并将它们添加到相应的类中(也就是说,所有单元测试都添加在UnitTestSuite
)。 这也意味着,当您在给定类别中编写新测试时,必须以编程方式将它们添加到适当的TestSuite
,当然还要重新编译它们。
然后,运行单个TestSuites
成为创建唯一的Ant任务(调用正确的测试集合)的一种练习。 您可以定义一个component-test
任务,以拾取ComponentTestSuite
等,如清单2所示:
清单2.仅运行组件测试的Ant任务
<target name="component-test"
if="Junit.present"
depends="junit-present,compile-tests">
<mkdir dir="${testreportdir}"/>
<junit dir="./" failureproperty="test.failure"
printSummary="yes"
fork="true" haltonerror="true">
<sysproperty key="basedir" value="."/>
<formatter type="xml"/>
<formatter usefile="false" type="plain"/>
<classpath>
<path refid="build.classpath"/>
<pathelement path="${testclassesdir}"/>
<pathelement path="${classesdir}"/>
</classpath>
<batchtest todir="${testreportdir}">
<fileset dir="test">
<include name="**/ComponentTestSuite.java"/>
</fileset>
</batchtest>
</junit>
</target>
理想情况下,您还将具有调用单元测试和系统测试的任务。 最后,对于需要运行整个测试套件的场合,您将创建依赖于所有三个测试类别的第四项任务,如清单3所示:
清单3.所有测试的Ant任务
<target name="test-all" depends="unit-test,component-test,system-test"/>
创建自定义TestSuite
是实现测试分类的快速解决方案。 该方法的缺点是,在创建新测试时,必须以编程方式将它们添加到适当的TestSuite
,这可能会很麻烦。 为每种类型的测试创建自定义目录更具扩展性,并允许您添加新的分类测试而无需重新编译。
创建自定义目录
我发现用JUnit实现测试分类的最简单方法是将测试逻辑上划分为与测试类型相对应的特定目录。 使用这种技术,所有单元测试将驻留在单元目录中,所有组件测试将驻留在组件目录中,依此类推。
例如,在保留所有未分类测试的测试目录中,您可以创建三个新的子目录,如清单4所示:
清单4.实现测试类别的目录结构
acme-proj/
test/
unit/
component/
system/
conf/
要运行这些测试,必须至少定义四个Ant任务:一个任务用于单元测试,另一个任务用于组件测试,依此类推。 第四个任务是运行所有三种测试类型的便捷任务(如清单3所示)。
JUnit任务非常类似于清单2中定义的任务。 但是,不同之batchtest
在于该任务的batchtest
方面。 这次, fileset
指向特定目录。 对于清单5,它指向unit目录:
清单5.运行所有单元测试的JUnit任务的batchtest方面
<batchtest todir="${testreportdir}">
<fileset dir="test/unit">
<include name="**/**Test.java"/>
</fileset>
</batchtest>
请注意,此测试仅运行test / unit目录中的所有测试。 创建新的单元测试(或与此有关的任何其他测试)时,只需要将它们放入目录中,一切就可以进行了! 这比必须在TestSuite
文件中添加新行并重新编译要容易一些。
问题解决了!
回到我们的原始场景,假设您和您的团队决定使用特定目录是构建时问题的最可扩展解决方案。 该任务最困难的方面是检查和分配测试类型。 您可以重构Ant构建文件并创建四个新任务(三个用于单个测试类型,另一个用于运行所有任务)。 此外,您修改了CruiseControl,使其仅在每小时一次的签入和组件测试中运行真正的单元测试。 经过进一步检查,结果表明系统测试也可以每小时运行一次,因此您创建了一个附加任务,可以同时运行组件测试和系统测试。
最终结果是一天要进行多次测试,您的团队可以更快地发现集成错误-通常在几个小时内。
创建构建敏捷性并非完全时髦,但它无疑在确保代码质量方面起着至关重要的作用。 随着您的测试越来越频繁地运行,对开发人员测试价值的担忧已成为遥不可及的记忆。 最重要的是,现在是2006年,您的公司取得了巨大的成功!
翻译自: https://www.ibm.com/developerworks/java/library/j-cq10316/index.html