Junit4拓展工具JCategory与Testng的Group功能比较

前言##

笔者前段时间分享过一遍文章,关于如何通过引入新注解来扩展Junit4,以解决Process上的问题:

JUnit扩展:引入新注解Annotation

最近在跟外面的同事聊的时候,得知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的实现基本上就是两大块:

  1. 如何找到所有Test Class?

主要是在运行时得到ClassPath路径String classPath = System.getProperty(getClasspathProperty()); 然后遍历该路劲下所有的文件来查找Test Class

  1. 如何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); 这行代码实现的。 其基本思路是这样的:

  1. 找到所有的ClassLoaders
  2. 遍历每一个Class Loader,然后通过方法 Enumeration<URL> dirEnumeration = classLoader.getResources(packageDirName) 来得到符合期望资源的URL
  3. 然后遍历所有URL,及其子元素,得到所有资源
  4. 遍历每一个资源,根据其文件类型-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⊙)?


posted @   大卡尔  阅读(1337)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示