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