`
xubindehao
  • 浏览: 240093 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

关于junit源码学习之发散思维:打造自己的单元测试容器——Junit Runner扩展详解

阅读更多

概述
    Junit是我们在单元测试中最常用的工具 包之一, 虽然该工具包十分简洁, 而且随后市面上也出现了各种测试工具和测试框架, 但是依然难撼其在单元测试领域的王者地位.
Junit4.x Runner剖析
     junit3.x和junit4.x是两个非常不同的版本, 不能简单的理解为是后者是前者的一个升级, 二者的内部实现有很大的不同。 这里只针对junit4.x以后的版本。
所有的testcase都是在Runner下执行的,可以将Runner理解为junit运行的容器,默认情况下junit会使用 JUnit4ClassRunner作为所有testcase的执行容器。如果要定制自己的junit,则需要实现自己的Runner,最简单的办法就是 从Junit4ClassRunner继承, spring-test,unitils这些框架就是采用这样的做法。如在spring中是SpringJUnit4ClassRunner,在 unitils中是UnitilsJUnit4TestClassRunner,一般我们的testcase都是在通过eclipse插件 来执行的,eclipse的junit插件会在执行的时候会初始化指定的Runner。初始化的过程可以在ClassRequest中找到:
   1. @Override
   2.     public Runner getRunner() {
   3.         return buildRunner(getRunnerClass(fTestClass));
   4.     }
   5.
   6.     public Runner buildRunner(Class< ? extends Runner> runnerClass) {
   7.         try {
   8.             return runnerClass.getConstructor(Class.class).newInstance(new Object[] { fTestClass });
   9.         } catch (NoSuchMethodException e) {
  10.             String simpleName= runnerClass.getSimpleName();
  11.             InitializationError error= new InitializationError(String.format(
  12.                     CONSTRUCTOR_ERROR_FORMAT, simpleName, simpleName));
  13.             return Request.errorReport(fTestClass, error).getRunner();
  14.         } catch (Exception e) {
  15.             return Request.errorReport(fTestClass, e).getRunner();
  16.         }
  17.     }
  18.
  19.     Class< ? extends Runner> getRunnerClass(final Class< ?> testClass) {
  20.         if (testClass.getAnnotation(Ignore.class) != null)
  21.             return new IgnoredClassRunner(testClass).getClass();
  22.         RunWith annotation= testClass.getAnnotation(RunWith.class);
  23.         if (annotation != null) {
  24.             return annotation.value();
  25.         } else if (hasSuiteMethod() && fCanUseSuiteMethod) {
  26.             return AllTests.class;
  27.         } else if (isPre4Test(testClass)) {
  28.             return JUnit38ClassRunner.class;
  29.         } else {
  30.             return JUnit4ClassRunner.class;
  31.         }
  32.     }  
这里的局部变量fTestClass是当前的testcase类.通过getRunner()方法可以获取Runner,该Runner默认情况下是 Junit4ClassRunner, 当然也可以是自己的Runner, 只要从Runner继承即可,getRunnerClass()是取得具体的Runner class的方法,在junit4.x中最简单的方式就是通过注解@RunWith来获取.所以要定制的话,最方便的做法就是通过@RunWith指定定 制的Runner, Spring-test, Unitils都是这么干的^_^
下面来看JUnit4ClassRunner的构造器:
   1. public JUnit4ClassRunner(Class< ?> klass) throws InitializationError {
   2.         fTestClass= new TestClass(klass);
   3.         fTestMethods= getTestMethods();
   4.         validate();
   5.     }  
JUnit4ClassRunner没有默认的构造器, 从构造器中我们可以看出, 它需要一个参数, 这个参数就是我们当前要运行的testcaseclass, Runner拿到了要执行的testcase类之后, 就可以进一步拿到需要执行的测试方法, 这个是通过注解拿到的:

   1. protected List getTestMethods() {
   2.     return fTestClass.getTestMethods();
   3. }
   4.
   5. List getTestMethods() {
   6.     return getAnnotatedMethods(Test.class);
   7. }
   8.
   9. public List getAnnotatedMethods(Class< ? extends Annotation> annotationClass) {
  10.     List results= new ArrayList();
  11.     for (Class< ?> eachClass : getSuperClasses(fClass)) {
  12.         Method[] methods= eachClass.getDeclaredMethods();
  13.         for (Method eachMethod : methods) {
  14.             Annotation annotation= eachMethod.getAnnotation(annotationClass);
  15.             if (annotation != null && ! isShadowed(eachMethod, results))
  16.                 results.add(eachMethod);
  17.         }
  18.     }
  19.     if (runsTopToBottom(annotationClass))
  20.         Collections.reverse(results);
  21.     return results;
  22. }

初始化完成之后, 就可以根据拿到的Runner, 调用其run方法,执行所有的测试方法了:

   1. @Override
   2. public void run(final RunNotifier notifier) {
   3.     new ClassRoadie(notifier, fTestClass, getDescription(), new Runnable() {
   4.         public void run() {
   5.             runMethods(notifier);
   6.         }
   7.     }).runProtected();
   8. }
   9.
  10. protected void runMethods(final RunNotifier notifier) {
  11.     for (Method method : fTestMethods)
  12.         invokeTestMethod(method, notifier);
  13. }
  14.
  15. protected void invokeTestMethod(Method method, RunNotifier notifier) {
  16.     Description description= methodDescription(method);
  17.     Object test;
  18.     try {
  19.         test= createTest();
  20.     } catch (InvocationTargetException e) {
  21.         notifier.testAborted(description, e.getCause());
  22.         return;
  23.     } catch (Exception e) {
  24.         notifier.testAborted(description, e);
  25.         return;
  26.     }
  27.     TestMethod testMethod= wrapMethod(method);
  28.     new MethodRoadie(test, testMethod, notifier, description).run();
  29. }

这里很多地方都利用了线程技术 , 可以忽略不管, 最终都是要通过反射拿到需要执行的测试方法并调用, 最终的调用在MethodRoadie中:

   1. public void run() {
   2.     if (fTestMethod.isIgnored()) {
   3.         fNotifier.fireTestIgnored(fDescription);
   4.         return;
   5.     }
   6.     fNotifier.fireTestStarted(fDescription);
   7.     try {
   8.         long timeout= fTestMethod.getTimeout();
   9.         if (timeout > 0)
  10.             runWithTimeout(timeout);
  11.         else
  12.             runTest();
  13.     } finally {
  14.         fNotifier.fireTestFinished(fDescription);
  15.     }
  16. }
  17.
  18. public void runTest() {
  19.     runBeforesThenTestThenAfters(new Runnable() {
  20.         public void run() {
  21.             runTestMethod();
  22.         }
  23.     });
  24. }
  25.
  26. public void runBeforesThenTestThenAfters(Runnable test) {
  27.     try {
  28.         runBefores();
  29.         test.run();
  30.     } catch (FailedBefore e) {
  31.     } catch (Exception e) {
  32.         throw new RuntimeException("test should never throw an exception to this level");
  33.     } finally {
  34.         runAfters();
  35.     }
  36. }
  37.
  38. protected void runTestMethod() {
  39.     try {
  40.         fTestMethod.invoke(fTest);
  41.         if (fTestMethod.expectsException())
  42.             addFailure(new AssertionError("Expected exception: " + fTestMethod.getExpectedException().getName()));
  43.     } catch (InvocationTargetException e) {
  44.         Throwable actual= e.getTargetException();
  45.         if (actual instanceof AssumptionViolatedException)
  46.             return;
  47.         else if (!fTestMethod.expectsException())
  48.             addFailure(actual);
  49.         else if (fTestMethod.isUnexpected(actual)) {
50. String message= "Unexpected exception, expected< " +fTestMethod.getExpectedException().getName() + "> but was< "
  51.                 + actual.getClass().getName() + ">“;
  52.             addFailure(new Exception(message, actual));
  53.         }
  54.     } catch (Throwable e) {
  55.         addFailure(e);
  56.     }
  57. }

spring-test应用参考
   下面是使用spring-test的runner如何来写testcase, 将会有不少简化(推荐 懒人使用):
    要测试的方法:

   1. public class ExampleObject {
   2.
   3.     public boolean getSomethingTrue() {
   4.         return true;
   5.     }
   6.
   7.     public boolean getSomethingFalse() {
   8.         return false;
   9.     }
  10. }

测试用例:

   1. @RunWith(SpringJUnit4ClassRunner.class)
   2. @ContextConfiguration(locations = { "classpath:/applicationContext.xml" })
   3. public class ExampleTest {
   4.     @Autowired
   5.     ExampleObject objectUnderTest;
   6.
   7.     @Test
   8.     public void testSomethingTrue() {
   9.         Assert.assertNotNull(objectUnderTest);
  10.         Assert.assertTrue(objectUnderTest.getSomethingTrue());
  11.     }
  12.
  13.     @Test
  14.     @Ignore
  15.     public void testSomethingElse() {
  16.         Assert.assertNotNull(objectUnderTest);
  17.         Assert.assertTrue(objectUnderTest.getSomethingFalse());
  18.     }
  19. }

xml配置:

   1. < ?xml version="1.0" encoding="gb2312"?>
   2. < !DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
   3.
   4.     
   5.     
   6.

如果是使用maven的话, pom.xml的配置:

   1.
   2.         
   3.             junit
   4.             junit
   5.             4.4
   6.         
   7.         
   8.             org.springframework
   9.             spring-test
  10.             2.5.5
  11.         
  12.         
  13.             org.springframework
  14.             spring-beans
  15.             2.5.4
  16.         
  17.         
  18.             org.springframework
  19.             spring-context
  20.             2.5.4
  21.         
  22.     

    需要注意的一点就是, 到spring2.5之后的版本对注解的支持才逐渐大面积的推广 开来, 因此使用的时候, 要注意spring的版本问题, 因为在我们的项目 中都是采用的2.0.7, 对于这个限制不免留下了一点遗憾.
实战
    看了spring的SpringJUnit4ClassRunner, 不得不让人手痒, 希望能定制自己的Runner.当然需要使用到java的annotation的相关知识.下面是在实际项目中结合二者的一个实战。
应用场景是这样的:我有一个测试工具类(DataGenerator)用来帮助初始化测试数据 和清除测试数据。该工具类需要两个配置文件 ,一个是数据源的配置文件,一个是用来初始化数据的excel数据表,我希望通过借助java的annotation和自定义Runner来实现这个功能。于是我写了下面的两个类, 一个是annotation类:

   1. @Retention(RetentionPolicy.RUNTIME)
   2. @Target( { ElementType.TYPE})
   3. public @interface DataGeneratorConfig {
   4.     /**
   5.      * jdbc配置文件
   6.      *
   7.      * @return
   8.      */
   9.     String dbConfig() default "db.config";
  10.
  11.     /**
  12.      * excel文件列表
  13.      *
  14.      * @return
  15.      */
  16.     String[] excelFiles() ;
  17. }

    很明显, 该类就是用来获取配置文件信息的。接下来是在junit运行起来之后, 且在执行测试方法之前根据配置文件初始化一些数据, 于是我从JUnit4ClassRunner继承, 写了下面的类:

   1. public class DataGeneratorJUnit4ClassRunner extends JUnit4ClassRunner {
   2.
   3.     public DataGeneratorJUnit4ClassRunner(Class< ?> clazz)
   4.             throws InitializationError {
   5.         super(clazz);
   6.     }
   7.
   8.     @Override
   9.     public void run(RunNotifier notifier) {
  10.         // 在运行前对DataGenerator进行初始化
  11.         initGenerator();
  12.         super.run(notifier);
  13.     }
  14.
  15.     /**
  16.      * 初始化DataGenerator
  17.      */
  18.     private void initGenerator() {
  19.         Class< ?> clazz = getTestClass().getJava Class();
  20.         while (clazz != null) {
  21.             DataGeneratorConfig annotation = clazz
  22.                     .getAnnotation(DataGeneratorConfig.class);
  23.
  24.             if (annotation != null) {
  25.                 String dbConfig = annotation.dbConfig();
  26.                 String[] excelFiles = annotation.excelFiles();
  27.
  28.                 try {
  29.                     DataGenerator.initCache(getAbsoluteExcelPaths(excelFiles),
  30.                             getAbsolutePath(dbConfig));
  31.                 } catch (Exception e) {
  32.                     throw new RuntimeException(”使用注解初始化DataGenerator失败”, e);
  33.                 }
  34.                 break;
  35.             }
  36.
  37.             clazzclazz = clazz.getSuperclass();
  38.         }
  39.     }
  40.
  41.     /**
  42.      * 取得excel文件绝对路径
  43.      * @param excelPaths
  44.      * @return
  45.      */
  46.     private String[] getAbsoluteExcelPaths(String[] excelPaths) {
  47.         String[] realPaths = new String[excelPaths.length];
  48.         for (int i = 0; i < excelPaths.length; i++) {
  49.             realPaths = getAbsolutePath(excelPaths);
  50.         }
  51.         return realPaths;
  52.     }
  53.
  54.     /**
  55.      * 根据文件名取得文件绝对路径
  56.      *
  57.      * @param fileName
  58.      * @return
  59.      */
  60.     private String getAbsolutePath(String fileName) {
  61.         return DataGeneratorJUnit4ClassRunner.class.getClassLoader().getResource(fileName)
  62.                 .getFile();
  63.     }
  64. }

    就这样我就可以借助annotation来完成初始化了, 在需要用到DataGenerator的testcase, 我可以这样写:

   1. @RunWith(DataGeneratorJUnit4ClassRunner.class)
   2. @DataGeneratorConfig(dbConfig = "config.properties", excelFiles = "xxx/yyy.xls")

    就这么简单, 再也不需要写java代码来进行初始化了, 通过配置就可以搞定.
小结
如果你有一些特殊的测试工具需要与Junit结合的话, 一般都可以通过定制自己的JunitRunner加入进来.比如这里将DataGenerator与Junit整合, spring也是一个很好的例子,他就是在junit的Runner中完成了spring的ApplicationContext初始化工作, 而不需要我们手动来处理.

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics