`
sun201200204
  • 浏览: 294927 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

打造自己的单元测试容器——Junit Runner扩展详解

    博客分类:
  • j2ee
阅读更多
http://rdc.taobao.com/blog/arch/2009/02/27/%e6%89%93%e9%80%a0%e8%87%aa%e5%b7%b1%e7%9a%84%e5%8d%95%e5%85%83%e6%b5%8b%e8%af%95%e5%ae%b9%e5%99%a8%e2%80%94%e2%80%94junit-runner%e6%89%a9%e5%b1%95%e8%af%a6%e8%a7%a3/
http://www.oobang.com/technology.bang?iframeUrl=/ArticleView.tr&groupId=344&menuId=5840&idInGroup=276&path=technology#


概述

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没有默认的构造器, 从构造器中我们可以看出, 它需要一个参数, 这个参数就是我们当前要运行的testcase class, 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().getJavaClass();
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[i] = getAbsolutePath(excelPaths[i]);
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结合的话, 一般都可以通过定制自己的Junit Runner加入进来.比如这里将DataGenerator与Junit整合, spring也是一个很好的例子, 他就是在junit的Runner中完成了spring的ApplicationContext初始化工作, 而不需要我们手动来处理.
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics