Junit4拓展工具JCategory与Testng的Group功能比较
前言##
笔者前段时间分享过一遍文章,关于如何通过引入新注解来扩展Junit4,以解决Process上的问题:
最近在跟外面的同事聊的时候,得知Testng中,Group的功能就能实现类似的效果,经过比较得知,果然如此!
再看JCategory##
在看Testng之前,先看看我的这个拓展,我叫它JCategory。 经过多次重构,现在结构看起来更清晰,更容易理解,并且加上了中英文的描述。 更重要的是所有代码已上传到Github:
传送门:JCategory
基本的使用方式是这样的:
首先我们可以在写Case时,用JCategory的注解来打上标签:
@Test
@Sprint("15.13")
@UserStory("US2011")
@Defect("1234")
public void testJCategory()
{
// Case Logic
}
这样就可以在执行时,用Include或者Exclude功能进行随意Filter测试用例。
比如,只跑Sprint 15.14的Test Case:
@RunWith(JCategory.class)
@IncludeSprint("15.14")
public class JCategoryTest {
}
或者,我想除了Spring 15.14,其他的Test Case都跑:
@RunWith(JCategory.class)
@ExcludeSprint("15.14")
public class JCategoryTest {
}
来看看Testng的Group功能##
看完了JCategory,我们来看看Testng.
官方文档:Test Groups
使用起来也非常简单:
@Test(groups = {"Sprint15.14", "US20134", "defect1234"})
public void testGroup(){
}
这样我们就可以用配置文件来标记哪些Test Case是我们想跑的。
同样,如果我们只想跑Sprint 15.14的Test Case,我们可以这样写我们的配置文件:
<suite name="Suite">
<test name="Test">
<groups>
<run>
<include name="Sprint15.14" />
</run>
</groups>
<packages>
<package name=".*"></package>
</packages>
</test> <!-- Test -->
</suite> <!-- Suite -->
或者仅仅不想跑Sprint 15.14的Test Case:
<suite name="Suite">
<test name="Test">
<groups>
<run>
<exclude name="Sprint15.14" />
</run>
</groups>
<packages>
<package name=".*"></package>
</packages>
</test> <!-- Test -->
</suite> <!-- Suite -->
两者从原理上讲有什么区别呢?##
JCategory的实现基本上就是两大块:
- 如何找到所有Test Class?
主要是在运行时得到ClassPath路径String classPath = System.getProperty(getClasspathProperty());
然后遍历该路劲下所有的文件来查找Test Class
- 如何Filter 上面找到的Test Class里面的Test Method?
通过自定义的各种条件,比如Sprint,UserStory,Defect来模拟在敏捷模式下,对Test Method的各种需求,然后继承org.junit.runner.manipulation.Filter
类来定义基于上面需求的规则,以让Junit根据这些规则来判断某个Test Method是否要执行,比如Sprint 的规则:
public class FilterSprint extends Filter
{
private String value;
private Sprint sprint;
public FilterSprint(String value)
{
this.value = value;
}
@Override
public boolean shouldRun(Description description)
{
if(description.isTest())
{
sprint = description.getAnnotation(Sprint.class);
return filterRule();
}
return true;
}
public boolean filterRule() {
if(value != null && (sprint == null || !value.equalsIgnoreCase(sprint.value())))
return false;
return true;
}
}
这里要提一下,研究过Junit源码的同学可能注意到,Junit本身是不提供查找Test Class的功能的,我们在使用Junit的框架时,要把Test Class传给Junit,然后它才能帮我们执行这个Test Class. (这里就涉及到Junit的入口在哪,具体可以参考我以前的一篇博客: Junit4的入口在哪?)
可能有同学要问,我在Eclipse里不用指定文件,直接选择Project然后右键Run As Junit Test,就可以直接跑所有Case,那是为什么呢?这其实是Eclipse上的Junit插件帮我们完成了找Test Class这个功能。
而Testng不同,它是可以基于XML文件来执行Case,所有它必须提供查找Test Class的功能。那Testng是如何做的呢?我找到了下面,相关功能的源码:
private List<XmlClass> initializeXmlClasses()
{
List<XmlClass> result= Lists.newArrayList();
try {
String[] classes = PackageUtils.findClassesInPackage(m_name, m_include, m_exclude);
int index = 0;
for(String className: classes) {
result.add(new XmlClass(className, index++, false /* don't load classes */));
}
}
catch(IOException ioex) {
Utils.log("XmlPackage", 1, ioex.getMessage());
}
return result;
}
Testng是基于XML里面的配置来查找Test Class的,具体实现是由String[] classes = PackageUtils.findClassesInPackage(m_name, m_include, m_exclude);
这行代码实现的。 其基本思路是这样的:
- 找到所有的ClassLoaders
- 遍历每一个Class Loader,然后通过方法
Enumeration<URL> dirEnumeration = classLoader.getResources(packageDirName)
来得到符合期望资源的URL - 然后遍历所有URL,及其子元素,得到所有资源
- 遍历每一个资源,根据其文件类型-file,jar或者bundleresource,来找出其中包含的Test Class。
详情可以参考源码:
public static String[] findClassesInPackage(String packageName,
List<String> included, List<String> excluded)
throws IOException
{
String packageOnly = packageName;
boolean recursive = false;
if (packageName.endsWith(".*")) {
packageOnly = packageName.substring(0, packageName.lastIndexOf(".*"));
recursive = true;
}
List<String> vResult = Lists.newArrayList();
String packageDirName = packageOnly.replace('.', '/') + (packageOnly.length() > 0 ? "/" : "");
Vector<URL> dirs = new Vector<>();
// go through additional class loaders
Vector<ClassLoader> allClassLoaders = new Vector<>();
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
allClassLoaders.add(contextClassLoader);
}
if (m_classLoaders != null) {
allClassLoaders.addAll(m_classLoaders);
}
int count = 0;
for (ClassLoader classLoader : allClassLoaders) {
++count;
if (null == classLoader) {
continue;
}
Enumeration<URL> dirEnumeration = classLoader.getResources(packageDirName);
while(dirEnumeration.hasMoreElements()){
URL dir = dirEnumeration.nextElement();
dirs.add(dir);
}
}
Iterator<URL> dirIterator = dirs.iterator();
while (dirIterator.hasNext()) {
URL url = dirIterator.next();
String protocol = url.getProtocol();
if(!matchTestClasspath(url, packageDirName, recursive)) {
continue;
}
if ("file".equals(protocol)) {
findClassesInDirPackage(packageOnly, included, excluded,
URLDecoder.decode(url.getFile(), "UTF-8"),
recursive,
vResult);
}
else if ("jar".equals(protocol)) {
JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.charAt(0) == '/') {
name = name.substring(1);
}
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
if (idx != -1) {
packageName = name.substring(0, idx).replace('/', '.');
}
if (recursive || packageName.equals(packageOnly)) {
//it's not inside a deeper dir
Utils.log("PackageUtils", 4, "Package name is " + packageName);
if (name.endsWith(".class") && !entry.isDirectory()) {
String className = name.substring(packageName.length() + 1, name.length() - 6);
Utils.log("PackageUtils", 4, "Found class " + className + ", seeing it if it's included or excluded");
includeOrExcludeClass(packageName, className, included, excluded, vResult);
}
}
}
}
}
else if ("bundleresource".equals(protocol)) {
try {
Class params[] = {};
// BundleURLConnection
URLConnection connection = url.openConnection();
Method thisMethod = url.openConnection().getClass()
.getDeclaredMethod("getFileURL", params);
Object paramsObj[] = {};
URL fileUrl = (URL) thisMethod.invoke(connection, paramsObj);
findClassesInDirPackage(packageOnly, included, excluded,
URLDecoder.decode(fileUrl.getFile(), "UTF-8"), recursive, vResult);
} catch (Exception ex) {
// ignore - probably not an Eclipse OSGi bundle
}
}
}
String[] result = vResult.toArray(new String[vResult.size()]);
return result;
}
至于Testng如何筛选Test Method,这个就比较简单了,当它得到所有Test Class时,就可以通过反射得到这些Class的所有Methods,和其Groups注解的对应值,然后用调用正则匹配就可以判断这个Test Method是否是期望的了,具体可以参考类org.testng.internal.MethodGroupsHelper
的实现。
从上面看JCategory和Testng解决的问题是一致的,但具体实现细节有所差别,总的来讲:
- JCategory是根据ClassPath路径来查找Test Class,而Testng是通过ClassLoader的getResource方法来确定路径的
- 对于如何Filter Test Method,因为JCategory只是Junit的一个拓展,自然而然的,它要用的Junit的功能,所以它使用了Junit的
org.junit.runner.manipulation.Filter
接口来定义规则。而Testng是自定义的,value都是String类型,所以它直接用正则匹配就可以判断是否期望,比如pattern.matcher(group).matches()
JCategory和Testng的Group功能哪个更好?##
好吧,已经很明显了,Testng更强大,更灵活,因为Groups里的value值可以自由匹配。不像JCategory只有预定义的几个固定注解。
但其实这也是Junit与Testng设计理念上的不同所导致的差异,Junit目标就是在单元测试领域,但是Testng希望适用于更丰富的测试场景。
所以这里建议,如果你的项目可以使用Testng,那最好。如果必须用Junit,并且有类似的需求,可以考虑下JCategory,或者是参考JCategory和Testng的Group功能,自己做一个Junit的Group拓展功能。
Contact me ?
Email: jinsdu@outlook.com
Blog: http://www.cnblogs.com/jinsdu/
Github: https://github.com/CarlJi
童鞋,如果觉得本文还算用心,还算有用,何不点个赞呢(⊙o⊙)?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步