JUnit擴展:引入新註解Annotation

發現問題

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的case
  • 運行某個Sprint下的某個UserStory所屬的Case
  • 運行全部的有Bug的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的實現,簡單不少,不須要包裝一些沒必要要的類了。

可是咱們仍然發現它還有兩個不方便的地方:

  • 要想跑哪些測試文件,必須把相應的測試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類,咱們看他的構造函數:

 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

 

 

 

若是您看了本篇博客,以爲對您有所收穫,請點擊下面的 [推薦]

若是您想轉載本博客,請註明出處[http://www.cnblogs.com/jinsdu/]

若是您對本文有意見或者建議,歡迎留言

相關文章
相關標籤/搜索