对JUnit 4和安卓测试代码的简单分析
Table of Contents
JUnit 4是一种流行的Java单元测试工具。使用JUnit 4和androidx.test包可以为Android程序编写单元测试和插桩测试。下面我对JUnit 4在Android程序上的使用做一些简单的分析。
1. JUnit 4中的3个核心概念
首先简单介绍一下JUnit 4。JUnit 4是一个单元测试框架,简单来说,JUnit 4的工作就是运行单元测试,然后将结果展示给用户。在这个过程中涉及3个核心概念:表示单元测试的Statement、运行单元测试的Runner,以及接收测试数据、展示结果的RunNotifier。
下面的代码摘录自org.junit.runners.ParentRunner,这段代码很好的展示了3个概念之间的关系:Runner运行Statement,将结果发给RunNotifier。
@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); } }
1.1. 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程序时,通常会加上
@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();
1.2. 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类贴在这里。
Listing 1: TestRule接口
public interface TestRule { Statement apply(Statement base, Description description); }
Listing 2: RunRules类
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; }
1.3. RunNotifier
Runner负责执行测试用例,测试的结果发送给RunNotifier。RunNotifier是RunListener集合,测试信息最终由RunListener处理。我们运行单元测试时,控制台输出的信息就是由TextListener生成的。
Listing 3: RunListener类
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(); } }
1.4. ParentRunner和BlockJUnit4ClassRunner
在前面介绍Runner、Statement和RunNotifier时,已经不止一次提到了BlockJUnit4ClassRunner和ParentRunner。这是两个非常重要的类,有了前面的基础,在这里我们可以做一些深入分析。
ParentRunner是一个抽象类,它有两个重要的函数:run和runLeaf,这两个函数在前面已经介绍过了。ParentRunner还有两个重要接口:
Listing 4: 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。
2. JUnit 4和Android
2.1. 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; }
2.2. AndroidTestRule和AndroidStatement
插桩测试通常需要在测试类中声明ActivityTestRule。
Listing 5: ActivityTestRule
@get:Rule val activityRule = ActivityTestRule(MainActivity::class.java)
ActivityTestRule将测试方法封装为AndroidStatement。AndroidStatement在运行前发送指令到设备,启动应用,并在测试后关闭应用。
Listing 6: 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); } }
2.3. 使用UiAutomator和Espresso进行测试
使用Espresso方法之前需要创建对应的ActivityTestRule对象。从上面的分析可以看到ActivityTestRule负责启动要测试的Activity。这一点可以通过UiAutomator接口手动实现。因此使用UiAutomator和Espresso进行测试只需要:
- 移除创建ActivityTestRule的代码。
- 调用UiAutomator接口手动启动Activity。
- 调用UiAutomator接口等待Activity启动完毕。
- 调用Espresso方法模拟用户操作。
2.4. androidx包中的其他规则
除了AndroidTestRule,androidx还提供了下列规则:
规则 | 说明 |
---|---|
GrantPermissionRule | 运行测试方法前申请权限。 |
ProviderTestRule | 测试ContentProvider。 |
ServiceTestRule | 测试服务。 |
PortForwardRule | 转发端口。 |
分析这些规则,只要看apply方法返回了什么Statement,以及这些Statement的evaluate做了什么。
3. 参考资料
4. 修订记录
- 2020年05月20日 建立文档。
- 2022年06月10日 修改部分文字;增加修订记录部分。
- 2023年08月15日 修改段落层级;增加使用UiAutomator和Espresso进行测试部分;增加参考资料;修改部分文字。