關於使用JUnit 4測試Android程序的一點分析

http://tommwq.tech/blog/%e5%85%b3%e4%ba%8e%e4%bd%bf%e7%94%a8junit-4%e6%b5%8b%e8%af%95android%e7%a8%8b%e5%ba%8f%e7%9a%84%e4%b8%80%e7%82%b9%e5%88%86%e6%9e%90/java

JUnit 4是一種流行的Java單元測試工具。使用JUnit 4和androidx.test包能夠爲Android程序編寫單元測試和Instrument測試。下面我對JUnit 4在Android程序上的使用作一些簡單的分析。android

1 JUnit 4中的3個核心概念

首先簡單介紹一下JUnit 4。JUnit 4是一個單元測試框架,簡單來講,JUnit 4的工做就是運行單元測試,而後將結果展現給用戶。在這個過程當中涉及3個核心概念:表示單元測試的Statement、運行單元測試的Runner,以及接收測試數據,展現結果的RunNotifier。api

下面的代碼摘錄自org.junit.runners.ParentRunner,這段代碼很好的展現了3個概念之間的關係:Runner運行Statement,將結果通知給RunNotifier。app

@Override
public void run(final RunNotifier notifier) {
    EachTestNotifier testNotifier = new EachTestNotifier(notifier, getDescription());
    try {
        Statement statement = classBlock(notifier);
        statement.evaluate();
    } catch (AssumptionViolatedException e) {
        testNotifier.addFailedAssumption(e);
    } catch (StoppedByUserException e) {
        throw e;
    } catch (Throwable e) {
        testNotifier.addFailure(e);
    }
}

2 Runner

org.junit.runner.Runner是一個抽象類,擁有兩個抽象方法:框架

public abstract Description getDescription();
public abstract void run(RunNotifier notifier);

一般咱們不會直接接觸到Runner類。若是想本身編寫Runner,能夠從org.junit.runners.BlockJUnit4ClassRunner派生。BlockJUnit4ClassRunner也是JUnit 4默認的Runner類。若是不想使用默認Runner,能夠在測試類上添加註解@RunWith,設置測試須要的Runner。在測試Android程序時,一般會加上ide

@RunWith(AndroidJUnit4::class)

這行代碼會使用androidx.test.ext.junit.runners.AndroidJUnit4做爲Runner。AndroidJUnit4是一個包裝類,它會檢查系統屬性java.runtime.name,若是其中包含字符串android,AndroidJUnit4會使用androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner做爲實際的Runner。不然將使用org.robolectric.RobolectricTestRunner。而AndroidJUnit4ClassRunner正式派生自BlockJUnit4ClassRunner。函數

這裏說一下@RunWith生效的過程。在運行測試的時候(好比Gradle使用的org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor任務),首先經過org.junit.runner.Request獲得ClassRequest對象,ClassRequest內部經過AllDefaultPossibilitiesBuilder按照下列順序工具

  • ignoredBuilder
  • annotatedBuilder
  • suiteMethodBuilder
  • junit3Builder
  • junit4Builder

逐一尋找合適的RunnerBuilder,而後構建出Runner。聽起來很複雜,實際上只需兩行代碼:單元測試

final Request request = Request.aClass((Class)testClass);
final Runner runner = request.getRunner();

3 Statement

Statement表示一個測試用例,測試用例只有一個動做:執行。所以Statement類很是簡單:測試

public abstract class Statement {
    public abstract void evaluate() throws Throwable;
}

若是測試失敗,Statement拋出異常。若是執行成功,no news is good news。Statement雖然簡潔,經過組合能夠構造出很是豐富的用法。好比下面這段代碼

protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) {
    List<FrameworkMethod> befores = getTestClass().getAnnotatedMethods(Before.class);
    return befores.isEmpty() ? statement : new RunBefores(statement, befores, target);
}

這段代碼摘自BlockJUnit4ClassRunner類。熟悉JUnit 4的人一看就會明白,這是處理@Before註解的地方。實際上,根據這段代碼,你們應該就能夠猜出RunBefores類大體是什麼樣子了。爲了例子的完整性,咱們把RunBefores類的代碼也展現在這裏。

package org.junit.internal.runners.statements;

import java.util.List;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;

public class RunBefores extends Statement {
    private final Statement next;

    private final Object target;

    private final List<FrameworkMethod> befores;

    public RunBefores(Statement next, List<FrameworkMethod> befores, Object target) {
        this.next = next;
        this.befores = befores;
        this.target = target;
    }

    @Override
    public void evaluate() throws Throwable {
        for (FrameworkMethod before : befores) {
            before.invokeExplosively(target);
        }
        next.evaluate();
    }
}

如同上面的@Before例子,JUnit 4的不少特性都是經過Statement組合實現的。下面這段代碼也是BlockJUnit4ClassRunner的一部分,從這裏咱們能夠看出,@Rule也是經過封裝Statement實現的。

protected Statement methodBlock(FrameworkMethod method) {
    Object test;
    try {
        test = new ReflectiveCallable() {
            @Override
            protected Object runReflectiveCall() throws Throwable {
                return createTest();
            }
        }.run();
    } catch (Throwable e) {
        return new Fail(e);
    }

    Statement statement = methodInvoker(method, test);
    statement = possiblyExpectingExceptions(method, test, statement);
    statement = withPotentialTimeout(method, test, statement);
    statement = withBefores(method, test, statement);
    statement = withAfters(method, test, statement);
    statement = withRules(method, test, statement);
    return statement;
}

爲了方便讀者參考,咱們把org.junit.rules.TestRule接口和RunRules類貼在這裏。

public interface TestRule {
    Statement apply(Statement base, Description description);
}
package org.junit.rules;

import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class RunRules extends Statement {
    private final Statement statement;

    public RunRules(Statement base, Iterable<TestRule> rules, Description description) {
        statement = applyAll(base, rules, description);
    }

    @Override
    public void evaluate() throws Throwable {
        statement.evaluate();
    }

    private static Statement applyAll(Statement result, Iterable<TestRule> rules,
            Description description) {
        for (TestRule each : rules) {
            result = each.apply(result, description);
        }
        return result;
    }
}

@BeforeClass註解也使用了Statement,下面的代碼來自ParentRunner。ParentRunner是BlockJUnit4ClassRunner的父類。

protected Statement classBlock(final RunNotifier notifier) {
    Statement statement = childrenInvoker(notifier);
    if (!areAllChildrenIgnored()) {
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
    }
    return statement;
}

4 RunNotifier

Runner負責執行測試用例,測試的結果通知給RunNotifier。RunNotifier是一個RunListener集合,測試信息最終由RunListener處理。咱們運行單元測試時,控制檯輸出的信息就是由TextListener生成的。

public class RunListener {
    public void testRunStarted(Description description) throws Exception {}
    public void testRunFinished(Result result) throws Exception {}
    public void testStarted(Description description) throws Exception {}
    public void testFinished(Description description) throws Exception {}
    public void testFailure(Failure failure) throws Exception {}
    public void testAssumptionFailure(Failure failure) {}
    public void testIgnored(Description description) throws Exception {}
}

在運行測試時,ParentRunner負責將測試事件通知給RunNotifier。

protected final void runLeaf(Statement statement, Description description, RunNotifier notifier) {
    EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
    eachNotifier.fireTestStarted();
    try {
        statement.evaluate();
    } catch (AssumptionViolatedException e) {
        eachNotifier.addFailedAssumption(e);
    } catch (Throwable e) {
        eachNotifier.addFailure(e);
    } finally {
        eachNotifier.fireTestFinished();
    }
}

5 ParentRunner和BlockJUnit4ClassRunner

在前面介紹Runner、Statement和RunNotifier時,已經不止一次提到了BlockJUnit4ClassRunner和ParentRunner。這是兩個很是重要的類,有了前面的基礎,在這裏咱們能夠作一些深刻分析。

ParentRunner是一個抽象類,它有兩個重要的函數:run和runLeaf,這兩個函數在前面已經介紹過了。ParentRunner還有兩個重要接口:

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

runChild負責執行一個測試方法。正如方法classBlock所暗示的,ParentRunner運行一個測試類。ParentRunner將一組測試方法看做本身的child。child經過getChildren得到。ParentRunner將各個child所表明的測試用例經過childrenInvoker封裝成一個Statement,在加上@BeforeClass和@AfterClass,構形成最終的Statement。

@Override
public void run(final RunNotifier notifier) {
    EachTestNotifier testNotifier = new EachTestNotifier(notifier, getDescription());
    try {
        Statement statement = classBlock(notifier);
        statement.evaluate();
    } catch (AssumptionViolatedException e) {
        testNotifier.addFailedAssumption(e);
    } catch (StoppedByUserException e) {
        throw e;
    } catch (Throwable e) {
        testNotifier.addFailure(e);
    }
}

protected Statement classBlock(final RunNotifier notifier) {
    Statement statement = childrenInvoker(notifier);
    if (!areAllChildrenIgnored()) {
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
    }
    return statement;
}

protected Statement childrenInvoker(final RunNotifier notifier) {
    return new Statement() {
        @Override
        public void evaluate() {
            runChildren(notifier);
        }
    };
}

private void runChildren(final RunNotifier notifier) {
    final RunnerScheduler currentScheduler = scheduler;
    try {
        for (final T each : getFilteredChildren()) {
            currentScheduler.schedule(new Runnable() {
                public void run() {
                    ParentRunner.this.runChild(each, notifier);
                }
            });
        }
    } finally {
        currentScheduler.finished();
    }
}

BlockJUnit4ClassRunner派生自ParentRunner<FrameworkMethod>。一樣如方法methodBlock所暗示,BlockJUnit4ClassRunner更關注測試方法層面的工做:根據註解尋找測試方法,並將測試方法封裝成Statement。

@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);
    }
}

protected Statement methodBlock(FrameworkMethod method) {
    Object test;
    try {
        test = new ReflectiveCallable() {
            @Override
            protected Object runReflectiveCall() throws Throwable {
                return createTest();
            }
        }.run();
    } catch (Throwable e) {
        return new Fail(e);
    }

    Statement statement = methodInvoker(method, test);
    statement = possiblyExpectingExceptions(method, test, statement);
    statement = withPotentialTimeout(method, test, statement);
    statement = withBefores(method, test, statement);
    statement = withAfters(method, test, statement);
    statement = withRules(method, test, statement);
    return statement;
}

這裏簡單說明一下methodBlock方法。FrameworkMethod表明一個Java方法,也就是@Test測試方法。methodBlock首先創建測試類實例,而後用methodInvoker將測試方法和類實例封裝成Statement,再加上@Before等註解,構造出完成的Statement。

6 AndroidJUnit4ClassRunner和UIThreadStatement

如今介紹一下Android單元測試中遇到的AndroidJUnit4ClassRunner。AndroidJUnit4ClassRunner派生自BlockJUnit4ClassRunner,核心代碼是重寫了方法methodInvoker。

@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
  if (UiThreadStatement.shouldRunOnUiThread(method)) {
    return new UiThreadStatement(super.methodInvoker(method, test), true);
  }
  return super.methodInvoker(method, test);
}

咱們知道methodInvoker將測試方法封裝爲Statement,AndroidJUnit4ClassRunner封裝的UIThreadStatement是作什麼用的呢?顧名思義,在UI線程中測試。

@Override
public void evaluate() throws Throwable {
  if (runOnUiThread) {
    final AtomicReference<Throwable> exceptionRef = new AtomicReference<>();
    runOnUiThread(
        new Runnable() {
          @Override
          public void run() {
            try {
              base.evaluate();
            } catch (Throwable throwable) {
              exceptionRef.set(throwable);
            }
          }
        });
    Throwable throwable = exceptionRef.get();
    if (throwable != null) {
      throw throwable;
    }
  } else {
    base.evaluate();
  }
}

shouldRunOnUiThread的判斷標準也很簡單,檢查是否有UiThread註解。

public static boolean shouldRunOnUiThread(FrameworkMethod method) {
  Class<? extends Annotation> deprecatedUiThreadTestClass = loadUiThreadClass("android.test.UiThreadTest");
  if (hasAnnotation(method, deprecatedUiThreadTestClass)) {
    return true;
  } else {
    // to avoid circular dependency on Rules module use the class name directly
    @SuppressWarnings("unchecked") // reflection
    Class<? extends Annotation> uiThreadTestClass = loadUiThreadClass("androidx.test.annotation.UiThreadTest");
    if (hasAnnotation(method, deprecatedUiThreadTestClass)
        || hasAnnotation(method, uiThreadTestClass)) {
        return true;
      }
  }
  return false;
}

7 AndroidTestRule和AndroidStatement

對於Instrument測試,還須要在測試類中聲明ActivityTestRule。

@get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)

ActivityTestRule將測試方法封裝爲AndroidStatement。AndroidStatement在運行前發送指令到設備,啓動應用。在測試後關閉應用。

@Override
public void evaluate() throws Throwable {
  MonitoringInstrumentation instrumentation =
      ActivityTestRule.this.instrumentation instanceof MonitoringInstrumentation
          ? (MonitoringInstrumentation) ActivityTestRule.this.instrumentation
          : null;
  try {
    if (activityFactory != null && instrumentation != null) {
      instrumentation.interceptActivityUsing(activityFactory);
    }
    if (launchActivity) {
      launchActivity(getActivityIntent());
    }
    base.evaluate();
  } finally {
    if (instrumentation != null) {
      instrumentation.useDefaultInterceptingActivityFactory();
    }

    T hardActivityRef = activity.get();
    if (hardActivityRef != null) {
      finishActivity();
    }
    activityResult = null;
    ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(lifecycleCallback);
  }
}

8 androidx包中的其餘規則

除了AndroidTestRule,androidx還提供了下列規則:

規則 說明
GrantPermissionRule 運行測試方法前申請權限。
ProviderTestRule 測試ContentProvider。
ServiceTestRule 測試服務。
PortForwardRule 轉發端口。

分析這些規則,只要看apply方法返回了什麼Statement,以及這些Statement的evaluate作了什麼。

相關文章
相關標籤/搜索