摘要
TestNG是一种基于注释的测试框架,通过添加诸如灵活的装置、测试分类、参数测试和依赖方法等特性来克服JUnit的一些不足之处。此外,TestNG运行于Java 5.0(通过注释)和Java 1.4(通过JavaDoc样式的注释)之上。由于TestNG可以轻松地将开发人员测试分类成单元、组件和系统组,因此能够使构建时间保持在可管理的范围内。通过使用group注释和多重Ant任务,测试组可以不同的频率运行于一台工作站之上或持续集成环境中。
本文分析了测试分类的概念,演示了如何将TestNG的group注释与灵活的测试装置具相结合,通过特定的Ant目标促进以不同频率运行的测试。本文假设您了解TestNG。
TestNG组的研究
TestNG支持一种直观的机制,用于分组测试类和相关测试方法。在最基本的层面上,TestNG的分组特性是通过test注释的groups参数启用的,它可附加到类或者单个方法。从其名称即可看出,一个类或单个方法可属于1至n组。
例如,下面的类包含两个公共方法,缺省标为测试并进一步分类为属于one组:
/**
* @testng.test groups="one"
*/
public class SimpleGroupedTest {
private String fixture;
/**
* @testng.before-class = "true"
*/
private void init(){
this.fixture = "Hello World";
}
public void verifyEquality(){
Assert.assertEquals("Hello World", this.fixture);
}
public void verifySame(){
String value = this.fixture;
Assert.assertSame(this.fixture, value);
}
}
相反,下一个类定义了两个测试方法。然而,一个方法却属于两个不同的组——one和two。相应地,任何相关装置逻辑都必须与其所需的一个组关联。本例中,在组one或组two执行之前,必须首先将init()方法配置为运行。
public class SimpleGroupedTwoTest {
private String fixture;
/**
* @testng.before-class = "true" \
* groups = "one, two"
*/
private void init(){
this.fixture = "Hello World";
}
/**
* @testng.test groups="one, two"
*/
public void verifyEqualityAgain(){
Assert.assertEquals(this.fixture, "Hello World");
}
/**
* @testng.test groups="two"
*/
public void verifySameAgain(){
String value = this.fixture;
Assert.assertSame(value, this.fixture);
}
}
TestNG支持以多种方式运行所需组,从通过TestNG Eclipse插件指定这些组一直到在TestNG Ant任务中列举它们。
测试分类简介
要验证正在工作的软件,最简单的方法之一就是执行一次构建(即编译源代码并执行测试);因此,长时间运行的构建是降低开发人员生产力的一项因素,这一点也不令人吃惊。不得不等待那超长的构建过程完成,几乎没有什么比这更恼人的了。可与之相提并论的就是编码过程中遇到意外的蓝屏和重启,但我们至少能够很容易地对时间较长的构建做点什么。
长时间构建的原因几乎总是测试执行这个步骤(除非是有数百万的.java 文件)。此外,如果存在大量的设置步骤,例如配置数据库或是部署一个 EAR 文件,执行一个测试套件的总时间倾向于变长。所以,精心设计一个测试分类策略并按照规定的时间间隔运行分类有利于获得可管理的构建持续时间。
然而,分类测试要求我们定义具体的分类,即细化单元测试。单元测试就像一张三层饼的一片,另两片则是组件测试和系统测试。下一节分析了开发人员通常会编写的不同类型的测试,诸如单元测试、组件测试和系统测试。随后,它们将在TestNG中执行,并集成到一个Ant 构建脚本里。
单元测试定义
单元测试验证独立对象的行为;然而,由于类的耦合,单元测试也能验证相关对象的行为。例如,下面的单元测试验证了对象身份,它是在TestNG中实现的,只关注一个类型:PartOfSpeechEnum。
/**
* @testng.test
*/
public class PartOfSpeechEnumTest {
public void verifyNotEquals() throws Exception{
assert PartOfSpeechEnum.ADJECTIVE !=
PartOfSpeechEnum.NOUN: "NOUN == ADJECTIVE?";
}
public void verifyEquals() throws Exception{
assert PartOfSpeechEnum.ADJECTIVE ==
PartOfSpeechEnum.ADJECTIVE "ADJECTIVE != ADJECTIVE";
}
}
有些时候,一个单元测试会验证多个对象的行为。然而,这些对象很少有其他的外部依赖项。例如,下面的测试验证了两个对象:Dependency和DependencyFinder。
//imports removed...
public class DependencyFinderTest {
private DependencyFinder finder;
/**
* @testng.test
*/
public void verifyFindDependencies() throws Exception{
String targetClss = "test.com.sedona.frmwrk.dep.Finder";
Filter[] filtr = new Filter[] {
new RegexPackageFilter("java|org")};
Dependency[] deps =
finder.findDependencies(targetClss, filtr);
Assert.assertNotNull(deps, "deps was null");
Assert.assertEquals(deps.length, 5, "should be 5 large");
}
/**
* @testng.before-class = "true"
*/
protected void init() throws Exception {
this.finder = new DependencyFinder();
}
}
要牢记的一个要点就是:单元测试不依靠外部依赖项,如数据库。数据库会增加设置和运行测试的时间。单元测试没有配置成本(就时间来度量),运行它的资源消耗可忽略不计。
单元测试运行很快,所以只要运行了一次构建,就应该运行单元测试,包括在持续集成环境中也是如此。在持续集成环境中,如果源储存库(如CVS)发生变更,通常就要运行构建。
组件测试
组件测试有几个别名,如子系统测试或集成测试。不管用哪个术语,这样的测试验证了应用程序的若干部分,甚至还可能需要一个完全安装的系统或一组更有限的外部依赖项,如数据库、文件系统、或网络端点。实质上,这些测试验证了不同组件交互以产生预期组合行为的过程。
典型的组件测试需要一个种子数据库(seeded database);此外,测试本身可能要跨架构边界来验证行为。由于组件测试要处理大量的代码,所以实现了更广泛的代码覆盖范围;但运行此类测试要比运行单元测试占用更长时间。
因为组件测试有相关成本——依赖项必须就位并被配置好,所以不该在每次执行构建时运行,而应以规定的时间间隔运行。记住,这些测试本身可能只需几秒钟,但更多的组件测试被添加到套件中时,整个测试时间就增加了,而且往往增加的非常快。
例如,下面的组件测试用DbUnit 播种一个底层数据库。这一测试用例中,设置本身所用的时间比大多数单元测试的运行时间都要长。
//imports removed... public class WordDAOImplTest { private WordDAO dao = null; /** * @testng.before-method = "true" */ private void setUp() throws Exception { final ApplicationContext context = new ClassPathXmlApplicationContext( "spring-config.xml"); this.dao = (WordDAO) context.getBean("wordDAO"); } /** * @testng.before-class = "true" */ public void oneTimeSetUp() throws Exception{ final IDatabaseConnection conn = this.getConnection(); final IDataSet data = this.getDataSet(); try{ DatabaseOperation.CLEAN_INSERT.execute(conn, data); }finally{ conn.close(); } } /** * @testng.test */ public void createWord() { final IWord word = new Word(); word.setSpelling("pabulum"); word.setPartOfSpeech( PartOfSpeechEnum.NOUN.getPartOfSpeech()); final IDefinition defOne = new Definition(); defOne.setDefinition("food"); defOne.setWord(word); final Set defs = new HashSet(); defs.add(defOne); word.setDefinitions(defs); try{ this.dao.createWord(word); }catch(CreateException e){ Assert.fail("CreateException was thrown"); } } private IDataSet getDataSet() throws IOException, DataSetException { return new FlatXmlDataSet( new File("test/conf/words-seed.xml")); } private IDatabaseConnection getConnection() throws ClassNotFoundException, SQLException { final Class driverClass = Class.forName("org.gjt.mm.mysql.Driver"); final Connection jdbcConnection = DriverManager. getConnection("jdbc:mysql://localhost/words", "words", "words"); return new DatabaseConnection(jdbcConnection); } }
但是,组件测试不总是依靠数据库。例如依靠文件系统创建一个耦合,这会增加配置的复杂性,在某些情况下,也会增加所需时间。举个例子,下面的组件测试使用XMLUnit 验证所生成的XML。注意这个测试依靠文件系统路径来比较两个XML文档。
//imports removed...
public class BatchDepXMLReportValidationTest {
/**
* @testng.before-class = "true"
*/
protected void configure() throws Exception {
XMLUnit.setControlParser(
"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
XMLUnit.setTestParser(
"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
XMLUnit.setSAXParserFactory(
"org.apache.xerces.jaxp.SAXParserFactoryImpl");
XMLUnit.setIgnoreWhitespace(true);
}
private Filter[] getFilters(){
return new Filter[] {
new RegexPackageFilter("java|org"),
new SimplePackageFilter("junit.")
};
}
private Dependency[] getDependencies(){
return new Dependency[] {
new Dependency("com.vanward.resource.XMLizable"),
new Dependency("com.vanward.xml.Element")
};
}
/**
* @testng.test
*/
public void assertToXML() throws Exception{
BatchDependencyXMLReport report =
new BatchDependencyXMLReport(
new Date(9000000), this.getFilters());
report.addTargetAndDependencies(
"com.vanward.test.MyTest", this.getDependencies());
report.addTargetAndDependencies(
"com.xom.xml.Test", this.getDependencies());
Diff diff = new Diff(new FileReader(
new File("./test/conf/report-control.xml")),
new StringReader(report.toXML()));
Assert.assertTrue(
diff.identical(),"XML was not identical");
}
}
虽然不应在每次运行构建时执行组件测试,但在将代码提交到储存库之前运行组件测试却是一个好主意。在持续集成环境中,时常运行它很可能是个好主意,比如每小时一次。
系统测试
系统测试处理一个完整的软件应用程序,验证外部接口,如Web页面、Web 服务端点、GUI、按照设计端到端地完成工作。因为系统测试处理的是整个系统,所以往往在开发的后期创建。除增加了设置和配置时间之外,系统测试还倾向于具有更长的执行时间。
例如,以下测试使用 jWebUnit 测试一个Web站点的登陆功能。请注意,这个测试中做了许多假设,如可用URL、实际上拥有有效账户的“tst”用户、未提及交易历史记录等。这些隐含的依赖项通常需要在测试运行前完成一个配置步骤。
public class LoginTest {
private WebTester tester;
/**
* @testng.before-class = "true"
*/
protected void init() throws Exception {
this.tester = new WebTester();
this.tester.getTestContext().
setBaseUrl("http://stocktrader.com");
}
/**
* @testng.test
*/
public void verifyLogIn() {
this.tester.beginAt("/");
this.tester.setFormElement("user", "tst");
this.tester.setFormElement("psswd", "t@t");
this.tester.submit();
this.tester.assertTextPresent("Logged in as tst");
}
/**
* @testng.test dependsOnMethods = "verifyLogIn"
*/
public void verifyAccountInfo() {
this.tester.clickLinkWithText("History", 0);
this.tester.assertTextPresent("$89.00, sold");
}
}
开发人员应当以需求为基础,在一个持续集成环境中本地运行这些测试。每晚执行测试是个不错的策略(如果测试能够实现自动运行)。更频繁地运行测试可能消耗大量系统资源,特别是在较大的环境中。但有了合理的硬件规划和更加完善的自动化,团队就能更频繁地运行这些测试。
实现TestNG分类
将TestNG测试分成三类就像使用上面所讲的group注释一样简单。通常不会有一个具有跨不同测试粒度的方法的测试类,所以在类的级别上,可以有效地应用标记。
例如,下面的类被标记为属于unit测试组。请注意,HierarchyBuilderTest是怎样依靠HierarchyBuilder类型来验证Hierarchy类的行为的。由于这一关系结束于不依赖文件系统或数据库的HierarchyBuilder,因此实际上可以把它看作单元测试:
import org.testng.Assert;
/**
* @testng.test groups="unit"
*/
public class HierarchyBuilderTest {
private Hierarchy hier;
/**
* @testng.before-class = "true" groups="unit"
*/
private void init() throws Exception{
this.hier =
HierarchyBuilder.buildHierarchy(Vector.class);
}
public void validateIsntNull() throws Exception{
Assert.assertNotNull(this.hier,
"should be something!");
}
/**
* @testng.test dependsOnMethods="validateIsntNull"
*/
public void validateValues() throws Exception{
Assert.assertEquals(
this.hier.getHierarchyClassNames().length,
2, "should be 2");
}
}
类似地,系统测试中单独的方法可以用system组标识来标记,示范如下:
public class LoginTest {
private WebTester tester;
/**
* @testng.before-class = "true" groups="system"
*/
protected void init() throws Exception {
this.tester = new WebTester();
this.tester.getTestContext().
setBaseUrl("http://acme.com:8080/ppo/");
}
/**
* @testng.test groups="system"
*/
public void verifyView() {
this.tester.beginAt("/");
this.tester.setFormElement("isbn", "900930390");
this.tester.submit();
this.tester.assertTextPresent("Book in stock");
}
}
运行分类测试
将代码签入内容管理系统之前,通过构建或像IDE那样的环境进行本地测试是极为重要的。通过TestNG Eclipse插件运行分类测试非常简单。如图1所示,通过在TestNG Create, manage, and run configurations 对话框中选择组选项,可用组的列表将出现,它带有复选框,这使选择变得更轻松。选好所需的一个或多个组以后,单击Run按钮,然后就看着绿色进度条一直向前!
图1,TestNG的Eclipse集成
通过构建,运行分类TestNG 测试将转变成为各组定义恰当的 Ant 目标。例如,为了运行属于组件组的所有测试,用指定的component组定义TestNG Ant 任务。
<target name="testng-component"
depends="testng-init">
<mkdir dir="${testng.output.dir.comp}"/>
<testng groups="component"
outputDir="${testng.output.dir.comp}"
sourceDir="${testng.source.dir}"
classpath="${testclassesdir};${classesdir}">
<classfileset dir="${testng.source.dir}">
<include name="**/*Test.java"/>
</classfileset>
<classpath>
<path refid="build.classpath"/>
</classpath>
</testng>
因此,采用这一策略,至少可以创建4个目标。其中3个分别对应单元、组件和系统测试,最后一个目标则能运行所有这3种测试。
结束语
TestNG使测试分类变得相当容易,这很可能是TestNG的最激动人心的优点之一。此外,TestNG的group注释还有助于把测试放到其他分类中,如成批测试、验收测试,甚至是性能测试。事实上,这一特性似乎已对最新版本的JUnit产生了影响,JUnit也在规划支持测试组!
下一次,当您摸索着编写新的测试用例时,考虑一下它对构建执行次数的长期影响。构建的可伸缩性以一个以各种频率运行的测试分类策略开始,有效的测试分类则以TestNG开始。
参考资料
- TestNG.org ——TestNG之家
- DbUnit ——使数据库依赖项管理(Database Dependency Management)变得更加轻松
- XMLUnit ——XML测试用例速成
- jWebUnit ——轻松完成系统测试