发现问题

JUnit提供了Test Suite来帮助我们组织case,还提供了Category来帮助我们来给建立大的Test Set,比如BAT,MAT, Full Testing。 那么什么情况下,这些仍然不能满足我们的需求,需要进行拓展呢?

闲话不表,直接上需求:

1. 老板希望能精确的衡量出每个Sprint写了多少条自动化case,或者每个User Story又设计了多少条case来保证覆盖率,以此来对工作量和效率有数据上的直观表示。 

2. 对于云服务,通常会有不同的server来对应产品不同的开发阶段,比如dev的server,这里大家可以随意上传代码,ScrumQA会测试每一个开发提交的Feature -> 然后当产品部署到测试server时,Scrum QA 和 System QA 就得对产品进行充分的测试 -> 甚或者有预发布的server,来模拟真实产品环境,这个server,仍然需要测试 -> 最后产品会部署到真正的生产环境上。 

这时候就衍生了测试代码版本控制的问题,比如当我针对新需求写的自动化case所测得Feature,当前只在Dev上部署,还没有在其他的server部署,那我的case怎么办?在其他的server上运行一定会失败。

3. 当产品足够复杂时,我们可能就有成千上万条case,这样全部运行这些case,时间上可能就非常耗时。 而如果使用JUnit的category来分组可能颗粒度太粗,不灵活,怎么办?

解决方案

综合考虑,为解决以上需求,我们觉得必须在Case级别上做文章,所以结合JUnit4的功能,我们最终引入了新的注解Annotation:Spint, UserStory, 和 Defect。

              名称                                     描述                                                  作用域                
             Sprint 用来标记这条Case是在什么时候引进的,或者说这条Case是哪个Sprint的加入的功能                方法
            UserStory 用来标记这条Case测试的是哪个UserStory,或者说覆盖的那个UserStory,这样即使过了很长时间,我们也能清晰地知道,我们为什么加这条Case,以及它到底测的是什么功能                 方法
             Defect 用来标记这条Case覆盖的Defect,或者说覆盖的Defect                方法

在实际使用时,我们就可以这么用:

  1. @Test
  2. @Sprint("15.3")
  3. @UserStory("US30145")
  4. @Defect(CR = "30775", Title = "[cr30775][p0] xxxx...")
  5. public void test_AddUserDirectDebitInformation_204_WithoutXXXBefore()
  6. {
  7. // Case Body
  8. }

那么在运行时,根据这些标记,我们就可以随心所欲的Filter出我们需要的Case了:

  • 只运行某个特定Sprint的case
  • 运行某个Sprint下的某个UserStory所属的Case
  • 运行所有的有Bug的Case,单做回归测试
  • 。。。

当然统计每个Sprint的工作量也就有了可能。

代码实现

研究过类似问题的童鞋可能知道,IBM的网站上,有一篇文章,讲过这个问题(地址在文章底有列)。并且他还定义了更多的注解需求, 也给出了部分实现。这篇文章有个好处就是先从JUnit整体架构运行流程出发,采用各个模块包装的方法,然后从入口JUnitCore出发去重新发起Request来运行我们自定义的Case,我觉得研究这篇文章可以对不了解JUnit核心框架的童鞋有一定的帮助。

这篇文章已经给出了自定义Runner部分的代码,缺了两块。第一是IntentObject部分,它把参数封装成对象。第二部分就是定义实现Filter。我这里给出它缺的那部分Filter的实现,Intent这块直接当String处理就可以了。

  1. public class FilterFactory
  2. {
  3. public static final String REGEX_COMMA = ",";
  4. public static final String ANNOTATION_PREFIX = "com.junit.extension.annotations."; // 包名的前缀
  5. public static final String REGEX_EQUAL = "=";
  6.  
  7. public static List<Filter> filters = new ArrayList<Filter>();
  8.  
  9. public static List<Filter> getFilters()
  10. {
  11. return filters;
  12. }
  13.  
  14. // This is a special toggle served {@link Sprint}
  15. public static String FILTER_RUN_CASE_ISONLY_TOGGLE = "isOnly";
  16.  
  17. private static FilterSprint fSprint = null;
  18. private static FilterUserStory fUserStory = null;
  19. private static FilterDefect fDefect = null;
  20.  
  21. public static List<Filter> createFilters(String intention) throws ClassNotFoundException
  22. {
  23. String[] splits = intention.split(REGEX_COMMA);
  24. for(String split : splits)
  25. {
  26. String[] pair = split.split(REGEX_EQUAL);
  27. if(pair != null && pair.length == 2)
  28. {
  29. if(pair[0].trim().equalsIgnoreCase(FILTER_RUN_CASE_ISONLY_TOGGLE))
  30. {
  31. if(fSprint == null)
  32. {
  33. fSprint = new FilterSprint();
  34. }
  35. fSprint.setIsOnly(Boolean.parseBoolean(pair[1].trim()));
  36. }
  37. else
  38. {
  39. Class<?> annotation = Class.forName(ANNOTATION_PREFIX + pair[0].trim());
  40.  
  41. if(annotation.isAssignableFrom(Sprint.class))
  42. {
  43. fSprint = new FilterSprint(pair[1].trim());
  44. }
  45. else if(annotation.isAssignableFrom(UserStory.class))
  46. {
  47. fUserStory = new FilterUserStory(pair[1].trim());
  48. filters.add(fUserStory);
  49. }
  50. else if(annotation.isAssignableFrom(Defect.class))
  51. {
  52. fDefect = new FilterDefect(pair[1].trim());
  53. filters.add(fDefect);
  54. }
  55. }
  56.  
  57. if(fSprint != null)
  58. {
  59. filters.add(fSprint);
  60. }
  61. }
  62. }
  63. return filters;
  64. }

然后再实现各个注解自定义Filter类,实现Filter的shouldRun方法定义, 以Sprint为例:

  1. /**
  2. * Filter rules for the annotation {@link Sprint}
  3. *
  4. * @author Carl Ji
  5. *
  6. */
  7. public class FilterSprint extends Filter
  8. {
  9. private String tgValue = null;
  10. private Boolean _isOnly = false;
  11.  
  12. public FilterSprint(String targetValue)
  13. {
  14. setTgValue(targetValue);
  15. }
  16.  
  17. public FilterSprint(String targetValue, Boolean isOnly)
  18. {
  19. setTgValue(targetValue);
  20. _isOnly = isOnly;
  21. }
  22.  
  23. public FilterSprint()
  24. {
  25. // TODO Auto-generated constructor stub
  26. }
  27.  
  28. public Boolean getIsOnly()
  29. {
  30. return _isOnly;
  31. }
  32.  
  33. public void setIsOnly(Boolean isOnly)
  34. {
  35. this._isOnly = isOnly;
  36. }
  37.  
  38. public String getTgValue()
  39. {
  40. return tgValue;
  41. }
  42.  
  43. public void setTgValue(String tgValue)
  44. {
  45. this.tgValue = tgValue;
  46. }
  47.  
  48. @Override
  49. public boolean shouldRun(FrameworkMethod method)
  50. {
  51. Sprint aSprint = method.getAnnotation(Sprint.class);
  52.  
  53. return filterRule(aSprint);
  54. }
  55.  
  56. @Override
  57. public boolean shouldRun(Description description)
  58. {
  59. if(description.isTest())
  60. {
  61. Sprint aSprint = description.getAnnotation(Sprint.class);
  62. return filterRule(aSprint);
  63. }
  64. else
  65. {
  66. return true;
  67. }
  68. }
  69.  
  70. @Override
  71. public String describe()
  72. {
  73. // TODO Auto-generated method stub
  74. return null;
  75. }
  76.  
  77. // Implement of filter rule for Sprint Annotation
  78. private boolean filterRule(Sprint aSprint)
  79. {
  80. if(_isOnly)
  81. {
  82. if(aSprint != null && aSprint.value().equalsIgnoreCase(tgValue))
  83. {
  84. return true;
  85. }
  86. else
  87. {
  88. return false;
  89. }
  90. }
  91. else
  92. {
  93. if(aSprint == null)
  94. {
  95. return true;
  96. }
  97. else
  98. {
  99. if(0 >= new StringComparator().compare(aSprint.value(), tgValue))
  100. {
  101. return true;
  102. }
  103. }
  104. }
  105.  
  106. return false;
  107.  
  108. }

我这里多了个isOnly属性,是为了解决历史遗留的问题。很多以前的Case并没有我们加上新自定义的注解,那么就可以通过这个属性来定义这些Case要不要执行。

当然核心的Filter的方法,还是要看大家各自的需求,自行设定。

优化,第二个解决办法

上面的实现方法做了很多工作,比如你要扩展JunitCore类,扩展Request 类,扩展RunnerBuilder类,还要扩展BlockJunit4ClassRunner类,那么研究过JUnit源码的童鞋可能知道,JUnit是提供入口让我们去注入Filter对象。具体是在ParentRunner类里的下面方法:

  1. //
  2. // Implementation of Filterable and Sortable
  3. //
  4.  
  5. public void filter(Filter filter) throws NoTestsRemainException {
  6. for (Iterator<T> iter = getFilteredChildren().iterator(); iter.hasNext(); ) {
  7. T each = iter.next();
  8. if (shouldRun(filter, each)) {
  9. try {
  10. filter.apply(each);
  11. } catch (NoTestsRemainException e) {
  12. iter.remove();
  13. }
  14. } else {
  15. iter.remove();
  16. }
  17. }
  18. if (getFilteredChildren().isEmpty()) {
  19. throw new NoTestsRemainException();
  20. }
  21. }

那其实我们只要把我们自定义的Filter对象传进来,我们的需求也就实现了。

  1. public class FilterCollections extends Filter
  2. {
  3. List<Filter> filters = null;
  4.  
  5. public FilterCollections(String intent)
  6. {
  7. try
  8. {
  9. filters = FilterFactory.createFilters(intent);
  10. }
  11. catch(ClassNotFoundException e)
  12. {
  13. e.printStackTrace();
  14. }
  15. }
  16.  
  17. @Override
  18. public boolean shouldRun(Description description)
  19. {
  20. List<Boolean> result = new ArrayList<Boolean>();
  21.  
  22. for(Filter filter : filters)
  23. {
  24. if(filter != null && filter.shouldRun(description))
  25. {
  26. result.add(true);
  27. }
  28. else
  29. {
  30. result.add(false);
  31. }
  32. }
  33.  
  34. if(result.contains(false))
  35. {
  36. return false;
  37. }
  38. else
  39. {
  40. return true;
  41. }
  42. }

这样就可以通过BlockJunit4ClassRunner直接调用:

  1. public class EntryToRunCases
  2. {
  3.  
  4. public static void main(String... args)
  5. {
  6. if(args != null)
  7. {
  8. System.out.println("Parameters: " + args[0]);
  9. Filter customeFilter = new FilterCollections(args[0]);
  10. EntryToRunCases instance = new EntryToRunCases();
  11. instance.runTestCases(customeFilter);
  12. }
  13. else
  14. {
  15. System.out.println("No parameters were input!");
  16. }
  17. }
  18.  
  19. protected void runTestCases(Filter aFilter)
  20. {
  21. BlockJUnit4ClassRunner aRunner = null;
  22. try
  23. {
  24. try
  25. {
  26. aRunner = new BlockJUnit4ClassRunner(JunitTest.class);
  27. }
  28. catch(InitializationError e)
  29. {
  30. System.out.print(e.getMessage());
  31. }
  32.  
  33. aRunner.filter(aFilter);
  34. aRunner.run(new RunNotifier());
  35. }
  36. catch(NoTestsRemainException e)
  37. {
  38. System.out.print(e.getMessage());
  39. }
  40. }
  41. }

这种方法要比上面IBM的实现,简单很多,不需要包装一些不必要的类了。

但是我们仍然发现它还有两个不方便的地方:

  • 要想跑哪些测试文件,必须把相应的测试Class,一条条加进来,这是JUnit固有的缺陷,Categories就是解决这个问题,但是它不识别我们自定义的注解
  • Case是通过Java Application Main方法来发起运行的,Eclipse IDE 的Junit 插件并不识别这种用法,所有我们没法在Eclipse的Junit窗口查看结果,只能通过Console打印出书出结果,这样可读性就差了很多

优化,更佳的解决方法

第一个问题很好解决,我们只要自己写方法来查找项目下的所有.java文件,匹配包含org.junit.Test.class注解的测试类就可以了,那第二个问题呢?

仔细思考我们的需求,我们会发现,我们并不想改变JUnit的Case执行能力,我们期望的只是希望JUnit能够只运行我们希望让它跑的Case. 而JUnit的Categories实现的就是这种功能。Categories继承自Suite类,我们看他的构造函数:

  1. public Categories(Class<?> klass, RunnerBuilder builder)
  2. throws InitializationError {
  3. super(klass, builder);
  4. try {
  5. filter(new CategoryFilter(getIncludedCategory(klass),
  6. getExcludedCategory(klass)));
  7. } catch (NoTestsRemainException e) {
  8. throw new InitializationError(e);
  9. }
  10. assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription());
  11. }

实际上就是把自定义的CategoryFilter传递给ParentRunner的filter方法,跟上面的方式,异曲同工。

Categories不识别我们的注解,那么我们是不是可以仿照它,做自己的Categories类呢?如下:

1。 首先定义使用自定义Categories的参数:

  1. @Retention(RetentionPolicy.RUNTIME)
  2. public @interface IncludeSprint
  3. {
  4. String value();
  5.  
  6. /*
  7. * This annotation will determine whether we want to run case without Sprint annotation or not
  8. * If not set, it is false by default
  9. */
  10. boolean isOnly() default false;
  11. }
  12.  
  13. @Retention(RetentionPolicy.RUNTIME)
  14. public @interface IncludeUserStory
  15. {
  16. String value();
  17. }
  18.  
  19. @Retention(RetentionPolicy.RUNTIME)
  20. public @interface IncludeDefect
  21. {
  22. String value();
  23. }

2。然后就可以在构造函数里针对它做处理:

  1. /**
  2. * Used by JUnit
  3. */
  4. public AnnotationClasspathSuite(Class<?> suiteClass, RunnerBuilder builder) throws InitializationError
  5. {
  6. super(builder, suiteClass, getTestclasses(new ClasspathClassesFinder(getClasspathProperty(suiteClass), new ClassChecker(
  7. getSuiteType(suiteClass))).find()));
  8. try
  9. {
  10. filter(new AnnotationsFilter(getIncludedSprint(suiteClass), getIncludedUserStory(suiteClass), getIncludedDefect(suiteClass),
  11. IsOnlyRunCaseWithSprintAnnotation(suiteClass)));
  12. }
  13. catch(NoTestsRemainException e)
  14. {
  15. throw new InitializationError(e);
  16. }
  17.  
  18. assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription());
  19. }

这样做出的自定义Suite Runner在使用上就非常方便了,比如:

  1. @RunWith(AnnotationClasspathSuite.class)
  2. @IncludeSprint(value = "15.3", isOnly = true)
  3. public class TestRunner
  4. {
  5. }

这样就可以明确的表明,我们只想跑Spring15.3的Case。并且结果也能在Eclipse IDE里完美展示:

总结

通过上面的实现,我们就能更细粒度的规划我们的case,也能按需灵活的运行自动化测试用例。

参考资料

实现这个期间,参考了不是好文章和代码,推荐童鞋们看看:

http://www.ibm.com/developerworks/cn/java/j-lo-junit4tdd/

http://highstick.blogspot.com/2011/11/howto-categorize-junit-test-methods-and.html

https://github.com/takari/takari-cpsuite

如果您看了本篇博客,觉得对您有所收获,请点击下面的 [推荐]

如果您想转载本博客,请注明出处[http://www.cnblogs.com/jinsdu/]

如果您对本文有意见或者建议,欢迎留言

JUnit扩展:引入新注解Annotation的更多相关文章

  1. 笔记9 AOP练习3(通过注解引入新功能 )

    切面可以为Spring bean添加新方法. 在Spring中,切面只是实现了它们所包装bean相同接口的 代理.如果除了实现这些接口,代理也能暴露新接口的话,会怎么样 呢?那样的话,切面所通知的be ...

  2. Spring系列之新注解配置+Spring集成junit+注解注入

    Spring系列之注解配置 Spring是轻代码而重配置的框架,配置比较繁重,影响开发效率,所以注解开发是一种趋势,注解代替xml配置文件可以简化配置,提高开发效率 你本来要写一段很长的代码来构造一个 ...

  3. spring beans源码解读之--Bean的注解(annotation)

    随着spring注解的引入,越来越多的开发者开始使用注解,这篇文章将对注解的机制进行串联式的讲解,不求深入透彻,但求串起spring beans注解的珍珠,展示给大家. 1. spring beans ...

  4. JAVA提高五:注解Annotation

    今天我们学习JDK5.0中一个非常重要的特性,叫做注解.是现在非常流行的一种方式,可以说因为配置XML 比较麻烦或者比容易查找出错误,现在越来越多的框架开始支持注解方式,比如注明的Spring 框架, ...

  5. 秒懂,Java 注解 (Annotation)你可以这样学 - CSDN博客

    https://blog.csdn.net/briblue/article/details/73824058 文章开头先引入一处图片. 这处图片引自老罗的博客.为了避免不必要的麻烦,首先声明我个人比较 ...

  6. JavaEE互联网轻量级框架整合开发(书籍)阅读笔记(10):通过注解(annotation)装配Bean之(@Configguration、@Component、@Value、@ComponentScan、@Autowired、@Primary、@Qualifier、@Bean)

    一.通过注解(annotation)装配Bean 通过之前的学习,我们已经知道如何使用XML装配Bean,但是更多的时候已经不再推荐使用XML的方式去装配Bean,更多的时候会考虑注解(annotat ...

  7. Spring实战(十)Spring AOP应用——为方法引入新功能、为对象引入新方法

    切面最基本的元素是通知和切点,切点用于准确定位应该在什么地方应用切面的通知. 1.Spring借助AspectJ的切点表达式语言来定义Spring切面 在Spring中,要使用AspectJ的切点表达 ...

  8. 主流的单元测试工具之-JAVA新特性-Annotation 写作者:组长 梁伟龙

    1:什么是Annotation?Annotation,即“@xxx”(如@Before,@After,@Test(timeout=xxx),@ignore),这个单词一般是翻译成元数据,是JAVA的一 ...

  9. 注解Annotation的IoC:从@Autowired到@Component

    注解Annotation的IoC:从@Autowired到@Component 2017-01-23 目录 1 什么是注解2 不使用注解示例  2.1 com.springioc.animal.Mon ...

随机推荐

  1. Win8控制面板快捷键

    Win8系统相比我们已经熟悉的Win7与XP系统有着一些特殊的变化,导致很多初次使用Win8系统的朋友感觉有点吃力,随着Win8系统即将于几天后正式发布,下面本文与大家分享大家比较关心的Win8控制面 ...

  2. 用WidgeDuino创建一个SCADA(监控与数据採集)系统

    WidgeDuino – 近期在Kickstarter上亮相 – 是一个智能的易配置的窗体- 基于Microsoft Windows平台和基于像 Atmel-based Arduino board 的 ...

  3. Hadoop-2.4.1学习之Map任务源代码分析(下)

    在Map任务源码分析(上)中,对MAP阶段的代码进行了学习,这篇文章文章将学习Map任务的SORT阶段.假设Reducer的数量不为0.则还须要进行SORT阶段.但从上面的学习中并未发现与MAP阶段运 ...

  4. mysql触发器的作用及语法

    触发器是一种特殊的存储过程,它在插入,删除或改动特定表中的数据时触发运行,它比数据库本身标准的功能有更精细和更复杂的数据控制能力. 数据库触发器有下面的作用: 1.安全性.能够基于数据库的值使用户具有 ...

  5. DataGrid缓冲加载数据

    当datagrid的滚动条拉到4/3的时候去加载数据.. public MainWindow() { InitializeComponent(); ; i <= ; i++) { Class1 ...

  6. MSDN无法显示该页的解决办法

    今天打开msdn,发现 查阅api时候 出现 “无法显示该页的解决办法“ 这个问题.解决方案如下: 在“运行”中输入regsvr32 "C:\Program Files\Common Fil ...

  7. Android 高级UI设计笔记01:使用ExpandableListView组件(ListView的扩展)

    1.ExpandableListView是一个用来显示二级节点的ListView. 比如如下效果的界面: 2.使用ExpandableListView步骤 (1)要给ExpandableListVie ...

  8. WebView实现文件下载功能

    WebView控制调用相应的WEB页面进行展示.安卓源码当碰到页面有下载链接的时候,点击上去是一点反应都没有的.原来是因为WebView默认没有开启文件下载的功能,如果要实现文件下载的功能,需要设置W ...

  9. CentOS 6.7下iPython提示“WARNING: Readline services not available or not loaded.”的解决办法

    yum install readline-devel 然后,使用pip或者easy_install安装readline即可 pip install readline

  10. python 简明教程笔记

    1,python特点 python 注重的是如何解决问题,而不是语法和结构简单高效.扩展性 2,安装 python python -V        检测是否安装pythonctrl+d       ...