JUnit 4.0在积极开发的长期中断之后于今年初发布。 通过巧妙地使用注释,可以对JUnit框架进行一些最有趣的更改,尤其是对于本专栏的读者而言。 除了从根本上更新外观外,新框架还大大简化了测试用例编写的结构规则。 先前的刚性夹具模型也已放松,以支持更具可配置性的方法。 结果,JUnit不再需要将测试定义为名称以test
开头的方法,现在您可以只运行一次夹具,而不是为每个测试运行夹具。
这些更改是最受欢迎的,但是JUnit 4并不是第一个提供基于注释的灵活模型的Java™测试框架。 在对JUnit进行修改之前,TestNG便将其自身建立为基于注释的框架。
实际上,TestNG在Java编程中率先使用批注进行测试,这使其成为JUnit的强大替代品。 但是,自从JUnit 4发行以来,许多开发人员都在问这两个框架之间是否还有任何区别。 在本月的专栏中,我将讨论一些使TestNG与JUnit 4脱颖而出的功能,并提出两个框架继续互补而不是竞争的方式。
表面相似
JUnit 4和TestNG有一些共同的重要属性。 这两个框架都使测试变得非常简单(有趣),从而简化了测试,它们都具有活跃的社区,在生成大量文档的同时支持积极的开发。
框架的不同之处在于其核心设计。 JUnit 一直是一个单元测试框架,这意味着它是为方便测试单个对象而构建的,并且这样做非常有效。 另一方面,TestNG旨在解决更高级别的测试,因此具有一些JUnit中不可用的功能。
一个简单的测试用例
乍一看,在JUnit 4和TestNG中实现的测试看起来非常相似。 要了解我的意思,请看清单1中的代码,它是一个具有宏夹具(在运行任何测试之前都会被调用一次的夹具)的JUnit 4测试,它由@BeforeClass
属性表示:
清单1.一个简单的JUnit 4测试用例
package test.com.acme.dona.dep;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.BeforeClass;
import org.junit.Test;
public class DependencyFinderTest {
private static DependencyFinder finder;
@BeforeClass
public static void init() throws Exception {
finder = new DependencyFinder();
}
@Test
public void verifyDependencies()
throws Exception {
String targetClss =
"test.com.acme.dona.dep.DependencyFind";
Filter[] filtr = new Filter[] {
new RegexPackageFilter("java|junit|org")};
Dependency[] deps =
finder.findDependencies(targetClss, filtr);
assertNotNull("deps was null", deps);
assertEquals("should be 5 large", 5, deps.length);
}
}
JUnit用户将立即注意到,该类缺少以前版本的JUnit所需的很多语法糖 。 没有setUp()
方法,该类没有扩展TestCase
,它甚至没有任何以test
开头的方法。 此类还利用了Java 5功能,例如静态导入和很明显的注释。
更大的灵活性
在清单2中,您将看到相同的测试 ,但是这次是使用TestNG实现的。 此代码与清单1中的测试之间有一个微妙的区别。您看到了吗?
清单2.一个TestNG测试用例
package test.com.acme.dona.dep;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Configuration;
import org.testng.annotations.Test;
public class DependencyFinderTest {
private DependencyFinder finder;
@BeforeClass
private void init(){
this.finder = new DependencyFinder();
}
@Test
public void verifyDependencies()
throws Exception {
String targetClss =
"test.com.acme.dona.dep.DependencyFind";
Filter[] filtr = new Filter[] {
new RegexPackageFilter("java|junit|org")};
Dependency[] deps =
finder.findDependencies(targetClss, filtr);
assertNotNull(deps, "deps was null" );
assertEquals(5, deps.length, "should be 5 large");
}
}
显然,这两个清单非常相似,但是如果仔细观察,您会发现TestNG编码约定比JUnit 4更为灵活。 在清单1中 ,JUnit迫使我将@BeforeClass
装饰的方法声明为static
,因此要求我也将我的灯具finder
声明为static
。 我还必须将init()
方法声明为public
。 查看清单2,您会看到一个不同的故事,因为这些约定不是必需的。 我的init()
方法既不是static
也不是public
。
从一开始,灵活性就一直是TestNG的优势之一,但这并不是其唯一的卖点。 TestNG还提供了一些在JUnit 4中找不到的测试功能。
依赖测试
JUnit框架试图实现的一件事是测试隔离。 不利的一面是,很难指定测试用例的执行顺序,这对于任何类型的依赖测试都是必不可少的。 开发人员已经使用了不同的技术来解决此问题,例如按字母顺序指定测试用例或严重依赖固定装置来正确地进行设置。
这些变通办法适用于成功的测试,但是对于失败的测试,它们会带来不便的后果: 每个后续的依赖测试也会失败。 在某些情况下,这可能导致大型测试套件报告不必要的故障。 例如,想象一下一个测试套件,该套件测试需要登录的Web应用程序。 您可以通过创建一个依赖方法来解决JUnit的隔离性,该方法通过登录应用程序来设置整个测试套件。 很好的解决问题的方法,但是当登录失败时,整个套件也会失败-即使该应用程序的登录后功能有效!
跳过,没有失败
与JUnit不同,TestNG通过Test
批注的dependsOnMethods
属性欢迎测试依赖项。 使用此便捷功能,您可以轻松指定相关方法,例如从上方登录,这些方法将在所需方法之前执行。 此外,如果从属方法失败,那么将跳过所有后续测试,而不是将其标记为失败。
清单3.使用TestNG的依赖测试
import net.sourceforge.jwebunit.WebTester;
public class AccountHistoryTest {
private WebTester tester;
@BeforeClass
protected void init() throws Exception {
this.tester = new WebTester();
this.tester.getTestContext().
setBaseUrl("http://div.acme.com:8185/ceg/");
}
@Test
public void verifyLogIn() {
this.tester.beginAt("/");
this.tester.setFormElement("username", "admin");
this.tester.setFormElement("password", "admin");
this.tester.submit();
this.tester.assertTextPresent("Logged in as admin");
}
@Test (dependsOnMethods = {"verifyLogIn"})
public void verifyAccountInfo() {
this.tester.clickLinkWithText("History", 0);
this.tester.assertTextPresent("GTG Data Feed");
}
}
在清单3中,定义了两个测试:一个用于验证登录名,另一个用于验证帐户信息。 请注意, verifyAccountInfo
测试使用Test
批注的dependsOnMethods = {"verifyLogIn"}
子句指定它依赖verifyLogIn()
方法。
如果通过TestNG的Eclipse插件运行了该测试(例如),并且verifyLogIn
测试失败,则TestNG会简单地跳过verifyAccountInfo
测试,如图1所示:
图1. TestNG中的跳过测试
TestNG跳过而不是失败的技巧实际上可以减轻大型测试套件中的压力。 您的团队可以专注于为何跳过50%的测试套件,而不是试图弄清楚为何50%的测试套件失败了! 更好的是,TestNG通过仅重新运行失败的测试的机制来补充其依赖性测试设置。
失败并重新运行
在大型测试套件中,重新运行失败的测试的功能尤其方便,这是您只能在TestNG中找到的功能。 在JUnit 4中,如果您的测试套件包含1000个测试,并且其中3个失败,那么您可能会被迫重新运行整个套件(包含修复程序)。 不用说,这种事情可能需要几个小时。
每当TestNG中发生故障时,它都会创建一个XML配置文件来描述失败的测试。 使用此文件运行TestNG运行程序会使TestNG 仅运行失败的测试。 因此,在前面的示例中,您只需要重新运行三个失败的测试,而不必重新运行整个套件。
实际上,您可以使用清单2中的Web测试示例亲自看到这一点。当verifyLogIn()
方法失败时,TestNG会自动创建一个testng-failed.xml文件。 该文件用作清单4中的替代测试套件:
清单4.测试XML文件失败
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite thread-count="5" verbose="1" name="Failed suite [HistoryTesting]"
parallel="false" annotations="JDK5">
<test name="test.com.acme.ceg.AccountHistoryTest(failed)" junit="false">
<classes>
<class name="test.com.acme.ceg.AccountHistoryTest">
<methods>
<include name="verifyLogIn"/>
</methods>
</class>
</classes>
</test>
</suite>
当您运行较小的测试套件时,此功能似乎没什么大不了的,但是随着测试套件规模的扩大,您很快就会体会到它的价值。
参数测试
参数测试是TestNG而非JUnit 4中提供的另一个有趣功能。 在JUnit中,如果要将参数组更改为要测试的方法,则必须为每个唯一组编写一个测试用例。 在大多数情况下,这并不是很不方便,但是每隔一段时间,您会遇到一个场景,其中业务逻辑需要大量变化的测试。
在这种情况下,JUnit测试人员经常转向FIT之类的框架,因为它使您可以使用表格数据进行测试。 但是TestNG提供了类似的功能。 通过将参数数据放置在TestNG的XML配置文件中,您可以将单个测试用例与不同的数据集一起重用,甚至获得不同的结果。 这项技术非常适合避免出现仅断言晴天场景或无法有效验证界限的测试。
在清单5中,我在Java 1.4中定义了一个TestNG测试,该测试接受两个参数,一个classname
和一个size
。 这些参数验证类的层次结构(即,如果我传入java.util.Vector
,则我希望HierarchyBuilder
构建2
的Hierarchy
)。
清单5.一个TestNG参数测试
package test.com.acme.da;
import com.acme.da.hierarchy.Hierarchy;
import com.acme.da.hierarchy.HierarchyBuilder;
public class HierarchyTest {
/**
* @testng.test
* @testng.parameters value="class_name, size"
*/
public void assertValues(String classname, int size) throws Exception{
Hierarchy hier = HierarchyBuilder.buildHierarchy(classname);
assert hier.getHierarchyClassNames().length == size: "didn't equal!";
}
}
清单5显示了一个通用测试,该测试可以一遍又一遍地用不同的数据重用。 考虑一下。 如果要在JUnit中测试10个不同的参数组合,则将不得不编写10个测试用例。 每个人基本上都会做同样的事情,只是改变被测方法的参数。 但是对于参数测试,您可以定义一个测试用例,然后将所需的参数模式(例如)推送到TestNG的套件文件中。 这就是清单6中我所做的:
清单6. TestNG中的一个参数套件文件
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd">
<suite name="Deckt-10">
<test name="Deckt-10-test">
<parameter name="class_name" value="java.util.Vector"/>
<parameter name="size" value="2"/>
<classes>
<class name="test.com.acme.da.HierarchyTest"/>
</classes>
</test>
</suite>
清单6中的TestNG套件文件仅定义用于测试的参数的一种组合( class_name
等于java.util.Vector
且size
等于2
),但是可能性是无限的。 另一个好处是,将测试数据移动到XML文件的非代码工件中,意味着非程序员也可以指定数据。
先进的参数测试
虽然将数据值提取到XML文件中非常方便,但测试有时需要复杂的类型,不能将其表示为String
或原始值。 TestNG通过其@DataProvider
注释来处理这种情况,该注释有助于将复杂的参数类型映射到测试方法。 例如,对于清单7中的verifyHierarchy
测试,我利用了一个覆盖的buildHierarchy
方法,该方法采用Class
类型,断言Hierarchy
的getHierarchyClassNames()
方法返回一个正确的String
数组:
清单7. TestNG中DataProvider的用法
package test.com.acme.da.ng;
import java.util.Vector;
import static org.testng.Assert.assertEquals;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.acme.da.hierarchy.Hierarchy;
import com.acme.da.hierarchy.HierarchyBuilder;
public class HierarchyTest {
@DataProvider(name = "class-hierarchies")
public Object[][] dataValues(){
return new Object[][]{
{Vector.class, new String[] {"java.util.AbstractList",
"java.util.AbstractCollection"}},
{String.class, new String[] {}}
};
}
@Test(dataProvider = "class-hierarchies")
public void verifyHierarchy(Class clzz, String[] names)
throws Exception{
Hierarchy hier = HierarchyBuilder.buildHierarchy(clzz);
assertEquals(hier.getHierarchyClassNames(), names,
"values were not equal");
}
}
dataValues()
方法通过多维数组提供数据值,该数组与verifyHierarchy
测试方法的参数值匹配。 TestNG遍历数据值,并因此两次调用verifyHierarchy
。 第一次,将Class
参数设置为Vector.class
,并且String
数组参数将两个"java.util.AbstractList"
和"java.util.AbstractCollection"
值保存为String
。 方便吗?
为什么选择一个?
我已经讨论了使TestNG与众不同的功能,但是JUnit中尚不可用。 例如,TestNG使用测试组,可以根据功能(例如运行时间)对测试进行分类。 如清单5所示 ,它还可以在Java 1.4中使用javadoc样式的注释。
正如我在本专栏文章的开头所说,JUnit 4和TestNG在表面上是相似的。 但是,尽管JUnit旨在磨合一个代码单元,但是TestNG是用于高级测试的。 它的灵活性在大型测试套件中特别有用,其中一个测试的失败并不意味着必须重新运行成千上万的套件。 每个框架都有其优势,没有什么可以阻止您同时使用两者。
翻译自: https://www.ibm.com/developerworks/java/library/j-cq08296/index.html