AndroidTest

Android 中单元测试并不常见, 这篇文章就我自己的知识范围来介绍:

JUnit

在学习 Java 时就知道这是一个用来给纯 Java 测试的工具, 在 Android 中一样使用, Android Studio 可以用快捷键 cmd + enter 为每个类自动创建测试类. 在测试类中, 一般使用 Assert 来断言每个条件的对错, 而 JUnit 的注解则为单元测试提供框架.

  • @Test

    定义测试单元(每个方法为一个单元用例), 接受参数有

    • expected 预期会抛出某个异常, 不抛出则报错

      1
      Class<? extends Throwable> expected() default None.class;
    • timeout 超时

      1
      long timeout() default 0L;
  • @Before

    每个 Test 方法执行之前都会调用, 可以做预处理操作, 必须修饰 public 方法

  • @After

    每个 Test 方法执行之后都会调用, 可以做清理操作

  • @BeforeClass

    由于连续测试可能需要共享一个变量, 或者每个测试单元执行前都需要很长时间的准备工作, 可以把这些准备工作移到 BeforeClass 中, 该注解必须注解于一个 public static void没有参数的方法.

  • @AfterClass

    用于 @BeforeClass 的清理工作

  • @Ignore

    用于执行测试的时候忽略某个 @Test 单元

  • @Rule

    定义 @Test 单元执行时的逻辑, org.junit.rules 包定义了一些常用的 Rule 规则, 但我们也可以自定义 Rule.

    1
    2
    3
    4
    5
    6
    7
    8
    public class RuleSample implements TestRule {

    @Override
    public Statement apply(Statement base, Description description) {
    base.evaluate();
    return null;
    }
    }

Mockito

Mock 即[模拟]的意思, 当某些类因为依赖太多等关系难以创造, 或者我们只需要一个类的对象时, Mock 便是一个很好的工具, 可以帮助我们隔离代码进行测试. 而 Mockito 便是一个 Android 常用的 Mock 工具类.

模拟对象

Mockito 支持多种模拟对象方法.

  • @Mock 直接注解对象. 注解对象需要初始化, 可以有四种初始化方法.

    • @Before 注解的初始化方法中使用MockitoAnnotations.initMocks(this);

    • 使用自带的 @Rule 初始化

      1
      2
      @Rule
      public MockitoRule mockitoRule = MockitoJUnit.rule();
    • 使用 @RunWith 注解测试类.

      1
      2
      3
      4
      @RunWith(MockitoJUnitRunner.class)
      public class Test {

      }
    • 使用 Mockito.mock() 初始化对象.

  • @InjectMock 创建一个实例, 其余用 @Mock@Spy 注解创建的 mock 对象将被注入到用该实例中.

设置桩

Mockito 支持以下方法设置桩

1
2
Mockito.doXXX().when(XXX)
Mockito.when(XXX).thenXXX()

举个例子, 如果希望 TextView 在调用 getText() 时返回特定的内容, 可以使用

1
2
Mockito.when(mTextView.getText()).thenReturn("123);
Mockito.doReturn("123").when(mTextView).getText();

桩可以设置多次, 最终只会返回最后一次设置的值.

验证

Mockito 支持以下方法来验证函数执行

1
2
public static <T> T verify(T mock)
public static <T> T verify(T mock, VerificationMode mode)

默认的 VerificationMode 即 times(1), 即方法执行了一次, sdk 也提供了几种默认的验证模式实现, 如 never() , atLeastOnce() 等.

参数匹配

为了模拟某些参数的输入, 可以匹配这些参数的输入, 并返回所需的值. ArgumentMatchers.any() 是一个比较常用的方法.

比如模拟 TextView 的 OnClickListener().

1
2
3
4
5
6
7
8
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
View.OnClickListener listener = invocation.getArgument(0);
listener.onClick(null);
return listener;
}
}).when(mTextView).setOnClickListener(ArgumentMatchers.any(View.OnClickListener.class));

@Spy

Mockito.spy() 返回的对象, 除非该方法已经有设置桩, 否则会调用该对象的真实方法. 可以用来改变对象特定方法的返回值, 而不改变对象本身. 注意 spy 不能 mock final 方法.

1
2
3
4
5
6
List list = new LinkedList();
List spy = Mockito.spy(list);
spy.add("one");
when(spy.size()).thenReturn(100);
spy.add("two");
System.out.println(spy.size());

JMockit

Mockito 的语法虽然比较简单易懂, 但它支持的功能还是不够多, 有一部分人是配合 PowerMock 一起使用, 但还有一个更强大的 Mock 工具值得使用.

模拟对象

注意 @RunWith 注解初始化测试类.

  • @Mocked 注解对象, 除了基本类型和数组对象, 其余所有都可以 mock, 会 mock 类中全部方法及其父类. 可以指定 stubOutClassInitialization 来决定 mock 对象时是否需要初始化静态变量, 某些 JNI 调用的静态变量在 mock 时初始化可能为 false, 这时候可以指定为 true 来跳过初始化.

  • @Injectable 仅 mock 指定的对象, 对于可以传入的对象, 使用 @Injectable 比 @Mock 更好

  • @Capturing mock 类及其子类, 也可以 mock 接口, 可以指定 maxInstances 来决定需要 mock 多少个对象.

常规使用

JMockit 用法是 录制 - 执行 - 验证 (record - replay - verify).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testMethod() throws Exception {

//record
new Expectations(){{
//执行方法, 可以是构造函数
a.getName();
//指定返回
result = "123";
}};

//run test code

//verify
new Verifications(){{

}};
}

参数匹配

参数匹配有很多, 最宽松的是 anyXXX , 其次是 withXXX.
使用 null 时, 必须有一个 anyXXX 或者 withXXX .

MockUp API

JMockit 我觉得 MockUp Api 特别强大, 几乎可以 Mock 所有的方法, 静态方法, 构造函数, 私有函数都可以, 这也是这个工具强大的地方.

比如有如下测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test {

public Test(String name) {

}

public static String getName() {

}

private String getAnswer() {

}
}

那么可以通过这样去 mock 对应的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new MockUp<Test>() {

@Mock
public void $init(Invocation invocation, String name) {
Test test = invocation.getInvokedInstance();
}

@Mock
public String getName() {
return "Mocked Name";
}

@Mock
private String getAnswer() {
return "Mocked Answer";
}
}

一些 Tip

  • 如果只是某个方法需要某个 mock 过的变量, 可以在测试方法的入参中传入, 不需要写成全局变量.

    1
    2
    3
    4
    @Test
    public void test(@Mocked TextView textView) {

    }