JUnit提供了Test Suite來幫助咱們組織case,還提供了Category來幫助咱們來給創建大的Test Set,好比BAT,MAT, Full Testing。 那麼什麼狀況下,這些仍然不能知足咱們的需求,須要進行拓展呢?html
閒話不表,直接上需求:java
1. 老闆但願能精確的衡量出每一個Sprint寫了多少條自動化case,或者每一個User Story又設計了多少條case來保證覆蓋率,以此來對工做量和效率有數據上的直觀表示。 git
2. 對於雲服務,一般會有不一樣的server來對應產品不一樣的開發階段,好比dev的server,這裏你們能夠隨意上傳代碼,ScrumQA會測試每個開發提交的Feature -> 而後當產品部署到測試server時,Scrum QA 和 System QA 就得對產品進行充分的測試 -> 甚或者有預發佈的server,來模擬真實產品環境,這個server,仍然須要測試 -> 最後產品會部署到真正的生產環境上。 github
這時候就衍生了測試代碼版本控制的問題,好比當我針對新需求寫的自動化case所測得Feature,當前只在Dev上部署,尚未在其餘的server部署,那個人case怎麼辦?在其餘的server上運行必定會失敗。架構
3. 當產品足夠複雜時,咱們可能就有成千上萬條case,這樣所有運行這些case,時間上可能就很是耗時。 而若是使用JUnit的category來分組可能顆粒度太粗,不靈活,怎麼辦?app
綜合考慮,爲解決以上需求,咱們以爲必須在Case級別上作文章,因此結合JUnit4的功能,咱們最終引入了新的註解Annotation:Spint, UserStory, 和 Defect。框架
名稱 | 描述 | 做用域 |
Sprint | 用來標記這條Case是在何時引進的,或者說這條Case是哪一個Sprint的加入的功能 | 方法 |
UserStory | 用來標記這條Case測試的是哪一個UserStory,或者說覆蓋的那個UserStory,這樣即便過了很長時間,咱們也能清晰地知道,咱們爲何加這條Case,以及它到底測的是什麼功能 | 方法 |
Defect | 用來標記這條Case覆蓋的Defect,或者說覆蓋的Defect | 方法 |
在實際使用時,咱們就能夠這麼用:ide
@Test @Sprint("15.3") @UserStory("US30145") @Defect(CR = "30775", Title = "[cr30775][p0] xxxx...") public void test_AddUserDirectDebitInformation_204_WithoutXXXBefore() { // Case Body }
那麼在運行時,根據這些標記,咱們就能夠爲所欲爲的Filter出咱們須要的Case了:函數
固然統計每一個Sprint的工做量也就有了可能。測試
研究過相似問題的童鞋可能知道,IBM的網站上,有一篇文章,講過這個問題(地址在文章底有列)。而且他還定義了更多的註解需求, 也給出了部分實現。這篇文章有個好處就是先從JUnit總體架構運行流程出發,採用各個模塊包裝的方法,而後從入口JUnitCore出發去從新發起Request來運行咱們自定義的Case,我以爲研究這篇文章能夠對不瞭解JUnit核心框架的童鞋有必定的幫助。
這篇文章已經給出了自定義Runner部分的代碼,缺了兩塊。第一是IntentObject部分,它把參數封裝成對象。第二部分就是定義實現Filter。我這裏給出它缺的那部分Filter的實現,Intent這塊直接當String處理就能夠了。
public class FilterFactory { public static final String REGEX_COMMA = ","; public static final String ANNOTATION_PREFIX = "com.junit.extension.annotations."; // 包名的前綴 public static final String REGEX_EQUAL = "="; public static List<Filter> filters = new ArrayList<Filter>(); public static List<Filter> getFilters() { return filters; } // This is a special toggle served {@link Sprint} public static String FILTER_RUN_CASE_ISONLY_TOGGLE = "isOnly"; private static FilterSprint fSprint = null; private static FilterUserStory fUserStory = null; private static FilterDefect fDefect = null; public static List<Filter> createFilters(String intention) throws ClassNotFoundException { String[] splits = intention.split(REGEX_COMMA); for(String split : splits) { String[] pair = split.split(REGEX_EQUAL); if(pair != null && pair.length == 2) { if(pair[0].trim().equalsIgnoreCase(FILTER_RUN_CASE_ISONLY_TOGGLE)) { if(fSprint == null) { fSprint = new FilterSprint(); } fSprint.setIsOnly(Boolean.parseBoolean(pair[1].trim())); } else { Class<?> annotation = Class.forName(ANNOTATION_PREFIX + pair[0].trim()); if(annotation.isAssignableFrom(Sprint.class)) { fSprint = new FilterSprint(pair[1].trim()); } else if(annotation.isAssignableFrom(UserStory.class)) { fUserStory = new FilterUserStory(pair[1].trim()); filters.add(fUserStory); } else if(annotation.isAssignableFrom(Defect.class)) { fDefect = new FilterDefect(pair[1].trim()); filters.add(fDefect); } } if(fSprint != null) { filters.add(fSprint); } } } return filters; }
而後再實現各個註解自定義Filter類,實現Filter的shouldRun方法定義, 以Sprint爲例:
/** * Filter rules for the annotation {@link Sprint} * * @author Carl Ji * */ public class FilterSprint extends Filter { private String tgValue = null; private Boolean _isOnly = false; public FilterSprint(String targetValue) { setTgValue(targetValue); } public FilterSprint(String targetValue, Boolean isOnly) { setTgValue(targetValue); _isOnly = isOnly; } public FilterSprint() { // TODO Auto-generated constructor stub } public Boolean getIsOnly() { return _isOnly; } public void setIsOnly(Boolean isOnly) { this._isOnly = isOnly; } public String getTgValue() { return tgValue; } public void setTgValue(String tgValue) { this.tgValue = tgValue; } @Override public boolean shouldRun(FrameworkMethod method) { Sprint aSprint = method.getAnnotation(Sprint.class); return filterRule(aSprint); } @Override public boolean shouldRun(Description description) { if(description.isTest()) { Sprint aSprint = description.getAnnotation(Sprint.class); return filterRule(aSprint); } else { return true; } } @Override public String describe() { // TODO Auto-generated method stub return null; } // Implement of filter rule for Sprint Annotation private boolean filterRule(Sprint aSprint) { if(_isOnly) { if(aSprint != null && aSprint.value().equalsIgnoreCase(tgValue)) { return true; } else { return false; } } else { if(aSprint == null) { return true; } else { if(0 >= new StringComparator().compare(aSprint.value(), tgValue)) { return true; } } } return false; }
我這裏多了個isOnly屬性,是爲了解決歷史遺留的問題。不少之前的Case並無咱們加上新自定義的註解,那麼就能夠經過這個屬性來定義這些Case要不要執行。
固然核心的Filter的方法,仍是要看你們各自的需求,自行設定。
上面的實現方法作了不少工做,好比你要擴展JunitCore類,擴展Request 類,擴展RunnerBuilder類,還要擴展BlockJunit4ClassRunner類,那麼研究過JUnit源碼的童鞋可能知道,JUnit是提供入口讓咱們去注入Filter對象。具體是在ParentRunner類裏的下面方法:
// // Implementation of Filterable and Sortable // public void filter(Filter filter) throws NoTestsRemainException { for (Iterator<T> iter = getFilteredChildren().iterator(); iter.hasNext(); ) { T each = iter.next(); if (shouldRun(filter, each)) { try { filter.apply(each); } catch (NoTestsRemainException e) { iter.remove(); } } else { iter.remove(); } } if (getFilteredChildren().isEmpty()) { throw new NoTestsRemainException(); } }
那其實咱們只要把咱們自定義的Filter對象傳進來,咱們的需求也就實現了。
public class FilterCollections extends Filter { List<Filter> filters = null; public FilterCollections(String intent) { try { filters = FilterFactory.createFilters(intent); } catch(ClassNotFoundException e) { e.printStackTrace(); } } @Override public boolean shouldRun(Description description) { List<Boolean> result = new ArrayList<Boolean>(); for(Filter filter : filters) { if(filter != null && filter.shouldRun(description)) { result.add(true); } else { result.add(false); } } if(result.contains(false)) { return false; } else { return true; } }
這樣就能夠經過BlockJunit4ClassRunner直接調用:
public class EntryToRunCases { public static void main(String... args) { if(args != null) { System.out.println("Parameters: " + args[0]); Filter customeFilter = new FilterCollections(args[0]); EntryToRunCases instance = new EntryToRunCases(); instance.runTestCases(customeFilter); } else { System.out.println("No parameters were input!"); } } protected void runTestCases(Filter aFilter) { BlockJUnit4ClassRunner aRunner = null; try { try { aRunner = new BlockJUnit4ClassRunner(JunitTest.class); } catch(InitializationError e) { System.out.print(e.getMessage()); } aRunner.filter(aFilter); aRunner.run(new RunNotifier()); } catch(NoTestsRemainException e) { System.out.print(e.getMessage()); } } }
這種方法要比上面IBM的實現,簡單不少,不須要包裝一些沒必要要的類了。
可是咱們仍然發現它還有兩個不方便的地方:
第一個問題很好解決,咱們只要本身寫方法來查找項目下的全部.java文件,匹配包含org.junit.Test.class註解的測試類就能夠了,那第二個問題呢?
仔細思考咱們的需求,咱們會發現,咱們並不想改變JUnit的Case執行能力,咱們指望的只是但願JUnit可以只運行咱們但願讓它跑的Case. 而JUnit的Categories實現的就是這種功能。Categories繼承自Suite類,咱們看他的構造函數:
public Categories(Class<?> klass, RunnerBuilder builder) throws InitializationError { super(klass, builder); try { filter(new CategoryFilter(getIncludedCategory(klass), getExcludedCategory(klass))); } catch (NoTestsRemainException e) { throw new InitializationError(e); } assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription()); }
實際上就是把自定義的CategoryFilter傳遞給ParentRunner的filter方法,跟上面的方式,殊途同歸。
Categories不識別咱們的註解,那麼咱們是否是能夠仿照它,作本身的Categories類呢?以下:
1。 首先定義使用自定義Categories的參數:
@Retention(RetentionPolicy.RUNTIME) public @interface IncludeSprint { String value(); /* * This annotation will determine whether we want to run case without Sprint annotation or not * If not set, it is false by default */ boolean isOnly() default false; } @Retention(RetentionPolicy.RUNTIME) public @interface IncludeUserStory { String value(); } @Retention(RetentionPolicy.RUNTIME) public @interface IncludeDefect { String value(); }
2。而後就能夠在構造函數裏針對它作處理:
/** * Used by JUnit */ public AnnotationClasspathSuite(Class<?> suiteClass, RunnerBuilder builder) throws InitializationError { super(builder, suiteClass, getTestclasses(new ClasspathClassesFinder(getClasspathProperty(suiteClass), new ClassChecker( getSuiteType(suiteClass))).find())); try { filter(new AnnotationsFilter(getIncludedSprint(suiteClass), getIncludedUserStory(suiteClass), getIncludedDefect(suiteClass), IsOnlyRunCaseWithSprintAnnotation(suiteClass))); } catch(NoTestsRemainException e) { throw new InitializationError(e); } assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription()); }
這樣作出的自定義Suite Runner在使用上就很是方便了,好比:
@RunWith(AnnotationClasspathSuite.class) @IncludeSprint(value = "15.3", isOnly = true) public class TestRunner { }
這樣就能夠明確的代表,咱們只想跑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