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

xiaoxiao2025-02-10  17

概述     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初始化工作, 而不需要我们手动来处理.

转载请注明原文地址: https://www.6miu.com/read-5024420.html

最新回复(0)