天天看點

Junit執行器Runner探索之旅

作者:京東雲開發者

單元測試是每個程式員必備的技能,而Runner是每個單元測試類必有屬性。本文通過解讀Junit源碼,介紹junit中每個執行器的使用方法,讓讀者在單元測試時,可以靈活的使用Runner執行器。

1 背景

在今年的靈活團隊建設中,京東物流通過Suite執行器實作了一鍵自動化單元測試。Juint除了Suite執行器還有哪些執行器呢?由此京東物流的Runner探索之旅開始了!

2 RunWith

RunWith的注釋是當一個類用@RunWith注釋或擴充一個用@RunWith注釋的類時,JUnit将調用它引用的類來運作該類中的測試,而不是内置到JUnit中的運作器,就是測試類根據指定運作方式進行運作。

代碼如下:

public @interface RunWith {
    Class<? extends Runner> value();
}           

其中:Runner 就是指定的運作方式。

3 Runner

Runner的作用是告訴Junit如何運作一個測試類,它是一個抽象類。通過RunWith 指定具體的實作類,如果不指定預設使用BlockJUnit4ClassRunner,Runner的代碼如下:

public abstract class Runner implements Describable {
    public abstract Description getDescription();
    public abstract void run(RunNotifier notifier);
    public int testCount() {
        return getDescription().testCount();
    }
}           

3.1 ParentRunner

ParentRunner是一個抽象類,提供了大多數特定于運作器的功能,是經常使用運作器的父節點。實作了Filterable,Sortable接口,可以過濾和排序子對象。

提供了3個抽象方法:

protected abstract List<T> getChildren();
protected abstract Description describeChild(T child);
protected abstract void runChild(T child, RunNotifier notifier);           

3.1.1 BlockJUnit4ClassRunner

BlockJUnit4ClassRunner是Juint4預設的運作器,具有與舊的測試類運作器(JUnit4ClassRunner)完全相同的行為。

ParentRunner3個抽象方法的實作如下:

@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
    Description description = describeChild(method);
    if (isIgnored(method)) {
        notifier.fireTestIgnored(description);
    } else {
        runLeaf(methodBlock(method), description, notifier);
    }
}
@Override
protected Description describeChild(FrameworkMethod method) {
    Description description = methodDescriptions.get(method);


    if (description == null) {
        description = Description.createTestDescription(getTestClass().getJavaClass(),
                testName(method), method.getAnnotations());
        methodDescriptions.putIfAbsent(method, description);
    }


    return description;
}
@Override
protected List<FrameworkMethod> getChildren() {
    return computeTestMethods();
}           

runChild() :

  • 調用describeChild()
  • 判斷方法是否包含@Ignore注解,有就觸發TestIgnored事件通知
  • 構造Statement回調,通過methodBlock()構造并裝飾測試方法
  • 執行測試方法調用statement.evaluate()

describeChild() : 對測試方法建立Description并進行緩存

getChildren():傳回運作測試的方法。 預設實作傳回該類和超類上所有用@Test标注的未重寫的方法

3.1.2 BlockJUnit4ClassRunnerWithParameters

BlockJUnit4ClassRunnerWithParameters是一個支援參數的BlockJUnit4ClassRunner。參數可以通過構造函數注入或注入到帶注釋的字段中。參數包含名稱、測試類和一組參數。

private final Object[] parameters;
private final String name;
public BlockJUnit4ClassRunnerWithParameters(TestWithParameters test)
        throws InitializationError {
    super(test.getTestClass().getJavaClass());
    parameters = test.getParameters().toArray(
            new Object[test.getParameters().size()]);
    name = test.getName();
}           

參數代碼如下:

public class TestWithParameters {
    private final String name;
    private final TestClass testClass;
    private final List<Object> parameters;
    public TestWithParameters(String name, TestClass testClass,
            List<Object> parameters) {
        notNull(name, "The name is missing.");
        notNull(testClass, "The test class is missing.");
        notNull(parameters, "The parameters are missing.");
        this.name = name;
        this.testClass = testClass;
        this.parameters = unmodifiableList(new ArrayList<Object>(parameters));
    }           

BlockJUnit4ClassRunnerWithParameters一般結合Parameterized使用

3.1.3 Theories

Theories允許對無限資料點集的子集測試某種功能。提供一組參數的排列組合值作為待測方法的輸入參數。同時注意到在使用Theories這個Runner的時候,待測方法可以擁有輸入參數,可以使您的測試更加靈活。

測試代碼如下:

@RunWith(Theories.class)
public class TheoriesTest {
    @DataPoints
    public static String[] tables = {"方桌子", "圓桌子"};
    @DataPoints
    public static int[] counts = {4,6,8};
    @Theory
    public void testMethod(String table, int count){
        System.out.println(String.format("一套桌椅有一個%s和%d個椅子", table, count));
    }
}           

運作結果:

Junit執行器Runner探索之旅

圖1 Theories測試代碼的執行結果

3.1.4 JUnit4

JUnit4是Junit4預設執行器的别名,想要顯式地将一個類标記為JUnit4類,應該使用@RunWith(JUnit4.class),而不是,使用@RunWith(BlockJUnit4ClassRunner.class)

3.1.5 Suite

Suite允許您手動建構包含來自許多類的測試的套件.通過Suite.SuiteClasses定義要執行的測試類,一鍵執行所有的測試類。

測試代碼如下:

@RunWith(Suite.class)
@Suite.SuiteClasses({Suite_test_a.class,Suite_test_b.class,Suite_test_c.class })
public class Suite_main {
}
public class Suite_test_a {
    @Test
    public void testRun(){
        System.out.println("Suite_test_a_running");
    }
}
public class Suite_test_b {
    @Test
    public void testRun(){
        System.out.println("Suite_test_b_running");
    }
}
public class Suite_test_c {
    @Test
    public void testRun(){
        System.out.println("Suite_test_c_running");
    }
}           

執行結果:

Junit執行器Runner探索之旅

圖2 Suite測試代碼的執行結果

如結果所示:執行MainSuit時依次執行了Suite_test_a,Suite_test_b,Suite_test_c 的方法,實作了一鍵執行。

3.1.6 Categories

Categories在給定的一組測試類中,隻運作用帶有@ inclecategory标注的類别或該類别的子類型标注的類和方法。通過ExcludeCategory過濾類型。

測試代碼如下:

public interface BlackCategory {}
public interface WhiteCategory {}


public class Categories_test_a {
    @Test
    @Category(BlackCategory.class)
    public void testFirst(){
        System.out.println("Categories_test_a_testFirst_running");
    }
    @Test
    @Category(WhiteCategory.class)
    public void testSecond(){
        System.out.println("Categories_test_a_testSecond_running");
    }
}


public class Categories_test_b {
    @Test
    @Category(WhiteCategory.class)
    public void testFirst(){
        System.out.println("Categories_test_b_testFirst_running");
    }
    @Test
    @Category(BlackCategory.class)
    public void testSecond(){
        System.out.println("Categories_test_b_testSecond_running");
    }
}           

執行帶WhiteCategory的方法

@RunWith(Categories.class)
@Categories.IncludeCategory(WhiteCategory.class)
@Categories.ExcludeCategory(BlackCategory.class)
@Suite.SuiteClasses( { Categories_test_a.class, Categories_test_b.class })
public class Categories_main {
}           

運作結果:

Junit執行器Runner探索之旅

圖4 Categories測試代碼WhiteCategory分組執行結果 圖3 Categories測試代碼WhiteCategory分組執行結果

執行帶BlackCategory的方法

@RunWith(Categories.class)
@Categories.IncludeCategory(BlackCategory.class)
@Categories.ExcludeCategory(WhiteCategory.class)
@Suite.SuiteClasses( { Categories_test_a.class, Categories_test_b.class })
public class Categories_main {
}           

運作結果:

Junit執行器Runner探索之旅

圖4 Categories測試代碼BlackCategory分組執行結果

如運作結果所示,通過IncludeCategory,ExcludeCategory可以靈活的運作具體的測試類和方法。

3.1.7 Enclosed

Enclosed使用Enclosed運作外部類,内部類中的測試将被運作。 您可以将測試放在内部類中,以便對它們進行分組或共享常量。

測試代碼:

public class EnclosedTest {
   @Test
    public  void runOutMethou(){
        System.out.println("EnclosedTest_runOutMethou_running");
    }
    public static class EnclosedInnerTest {
        @Test
       public  void runInMethou(){
            System.out.println("EnclosedInnerTest_runInMethou_running");
        }
    }
}           

運作結果:沒有執行内部類的測試方法。

Junit執行器Runner探索之旅

圖5 Enclosed測試代碼的執行結果

使用Enclosed執行器:

@RunWith(Enclosed.class)
public class EnclosedTest {
   @Test
    public  void runOutMethou(){
        System.out.println("EnclosedTest_runOutMethou_running");
    }
    public static class EnclosedInnerTest {
        @Test
       public  void runInMethou(){
            System.out.println("EnclosedInnerTest_runInMethou_running");
        }
    }
}           

執行結果:執行了内部類的測試方法。

Junit執行器Runner探索之旅

圖6 Enclosed測試代碼的執行結果

3.1.8 Parameterized

Parameterized實作參數化測試。 運作參數化的測試類時,會為測試方法和測試資料元素的交叉乘積建立執行個體。

Parameterized包含一個提供資料的方法,這個方法必須增加Parameters注解,并且這個方法必

須是靜态static的,并且傳回一個集合Collection,Collection中的值長度必須相等。

測試代碼:

@RunWith(Parameterized.class)
public class ParameterizedTest {
    @Parameterized.Parameters
    public static Collection<Object[]> initData(){
       return Arrays.asList(new Object[][]{
               {"小白",1,"雞腿"},{"小黑",2,"面包"},{"小紅",1,"蘋果"}
       });
    }
    private String name;
    private int  count;
    private String food;


    public ParameterizedTest(String name, int count, String food) {
        this.name = name;
        this.count = count;
        this.food = food;
    }
    @Test
    public void eated(){
       System.out.println(String.format("%s中午吃了%d個%s",name,count,food));
    }
}           

運作結果:

Junit執行器Runner探索之旅

圖7 Parameterized測試代碼的執行結果

3.2 JUnit38ClassRunner

JUnit38ClassRunner及其子類是Junit4的内部運作器,有一個内部類OldTestClassAdaptingListener

實作了TestListener接口。

3.3 ErrorReportingRunner

ErrorReportingRunner也是Junit4運作錯誤時抛出的異常,代碼如下:

private final List<Throwable> causes;


public ErrorReportingRunner(Class<?> testClass, Throwable cause) {
    if (testClass == null) {
        throw new NullPointerException("Test class cannot be null");
    }
    this.testClass = testClass;
    causes = getCauses(cause);
}


    private List<Throwable> getCauses(Throwable cause) {
        if (cause instanceof InvocationTargetException) {
            return getCauses(cause.getCause());
        }
        if (cause instanceof InitializationError) {
            return ((InitializationError) cause).getCauses();
        }
        if (cause instanceof org.junit.internal.runners.InitializationError) {
            return ((org.junit.internal.runners.InitializationError) cause)
                    .getCauses();
        }
        return Arrays.asList(cause);
    }           

當junit運作錯誤時,會抛出ErrorReportingRunner,例如:

public Runner getRunner() {
    try {
        Runner runner = request.getRunner();
        fFilter.apply(runner);
        return runner;
    } catch (NoTestsRemainException e) {
        return new ErrorReportingRunner(Filter.class, new Exception(String
                .format("No tests found matching %s from %s", fFilter
                        .describe(), request.toString())));
    }
}           

3.4 IgnoredClassRunner

IgnoredClassRunner是當測試的方法包含Ignore注解時,會忽略該方法。

public class IgnoredClassRunner extends Runner {
    private final Class<?> clazz;
    public IgnoredClassRunner(Class<?> testClass) {
        clazz = testClass;
    }
    @Override
    public void run(RunNotifier notifier) {
        notifier.fireTestIgnored(getDescription());
    }
    @Override
    public Description getDescription() {
        return Description.createSuiteDescription(clazz);
    }
}           

IgnoredClassRunner的使用

public class IgnoredBuilder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> testClass) {
        if (testClass.getAnnotation(Ignore.class) != null) {
            return new IgnoredClassRunner(testClass);
        }
        return null;
    }
}           

當測試時想忽略某些方法時,可以通過繼承IgnoredClassRunner增加特定注解實作。

4 小結

Runner探索之旅結束了,可是單元測試之路才剛剛開始。不同的Runner組合,讓單元測試更加靈活,測試場景更加豐富,更好的實作了測試驅動開發,讓系統更加牢固可靠。

繼續閱讀