1. 1. 技术点
    1. 1.1. RestrictTo注解
    2. 1.2. AndroidJUnitRunner
  2. 2. 实战

本文为 一旬一题写作计划 单元测试 专题内文章,读完本篇大约需要5分钟。


技术点

Service 涉及的 Android 类很多,所以要用设备测试(Instrumentation Test),也就是测试代码要放在 module-name/src/androidTest/java/。
本文的测试方案会用到 RestrictTo + Junit4 + AndroidJUnitRunner。
下面做个简单介绍:

RestrictTo注解

android.support.annotation 包下定义的一个注解:
>
Denotes that the annotated element should only be accessed from within a specific scope (as defined by RestrictTo.Scope).

用来表示被注解的元素只能在限定范围内(library,tests,子类)被访问。
比如只在测试中有效的方法,可以这样写。

1
2
@RestrictScope(TESTS)
public abstract int getUserId();

遗憾的是,目前这个注解只起到提示作用,即便不在限定范围内访问,也不会发生什么。不过即将到来的AndroidStudio 2.3 版本中这方面会加强,在Lint的时候会提出警告。期待期待~

AndroidJUnitRunner

设备测试本身不支持JUnit4。AndroidJUnitRunner 类是一个 JUnit 测试运行器,可让您在 Android 设备上运行 JUnit 3 或 JUnit 4 样式测试类。

  1. 添加依赖

    1
    2
    3
    4
    dependencies {
    compile 'com.android.support:support-annotations:22.2.0'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    }
  2. 指定 testInstrumentationRunner:

    1
    2
    3
    4
    5
     android {
    defaultConfig {
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    }
  3. 给测试类加上注解 :

    1
    @RunWith(AndroidJUnit4.class)
  4. 然后就可以写Junit4测试代码了。

(注意有些手机,比如 Oppo R9 ,Sony Xperia Z3,运行设备测试会报错“Unable to find instrumentation info”,只能换手机了)

实战

比如项目中的一个模块( 比如下面的 TheSDK ),初始化完成的时候,应该要启动了某个Service。这个逻辑要怎么测试?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TheSDK {

private static void init(@NonNull final Context context) {

// .....

startServices(context);

// .....
}

private static void startServices(final @NonNull Context context) {
context.startService(new Intent(context, Service1.class));
}

public static class Builder {
// .....
public void init() {
TheSDK.init(...);
}
}
}

首先用RestrictTo对Service1做一点改造,具体看注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Service1 extends Service {

@RestrictTo (RestrictTo.Scope.TESTS)
static boolean started;//增加一个只在测试中用的标志位

private SomeBroadCastReceiver receiver;

public Service1() {
}

@Override
public IBinder onBind(Intent intent) {
throw null;
}

@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
return START_STICKY;
}

@Override
public void onCreate() {
super.onCreate();
started = true;//标记启动完成
//...init broadcast receiver and whatnot
}

@Override
public void onDestroy() {
unregisterReceiver(receiver);
super.onDestroy();
}
}

然后给测试类加上注解,指定运行器为 AndroidJUnit4,写测试断言,搞定!:

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith (AndroidJUnit4.class) // <<----Make sure you add this!!!!
public class TheSDKTest {

@Test
public void test_services_started_on_init() throws TimeoutException {
final Context context = InstrumentationRegistry.getTargetContext();

new TheSDK.Builder(context).init();

assertTrue(Service1.started);
}
}

(Tips: Service的测试改造代码可以封装在BaseService中,就不用重复手写了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class BaseService extends Service {

@RestrictTo (RestrictTo.Scope.TESTS)
private static boolean isServiceStarted;

@Override
public IBinder onBind(Intent intent) {
throw null;
}

@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
return START_STICKY;
}

@Override
public void onCreate() {
super.onCreate();
isServiceStarted = true;
}

@Override
public void onDestroy() {
super.onDestroy();
isServiceStarted = false;
}

public static boolean isServiceStarted() {
return isServiceStarted;
}

}

/**
而且Service可以拎出来单独测试
**/

@RunWith (AndroidJUnit4.class)
public class BaseServiceTest {

private Context context;

@Before
public void setUp() {
context = InstrumentationRegistry.getTargetContext();
}

@Test (expected = NullPointerException.class)
public void onBind() throws Exception {
final BaseService myService = new BaseService();
context.startService(new Intent(context, BaseService.class));

myService.onBind(new Intent());
}

@Test
public void onStartCommand() throws Exception {
final BaseService myService = new BaseService();
context.startService(new Intent(context, BaseService.class));

assertEquals(Service.START_STICKY, BaseService.onStartCommand(new Intent(), 0, 0));
}

@Test
public void onCreate() throws Exception {
final BaseService myService = new BaseService();
context.startService(new Intent(context, BaseService.class));

myService.onCreate();
assertTrue(BaseService.isServiceStarted());
}

@Test
public void onDestroy() throws Exception {
final BaseService myService = new BaseService();
context.startService(new Intent(context, BaseService.class));

myService.onDestroy();
assertFalse(BaseService.isServiceStarted());
}
}

参考资料与扩展阅读

Google官方测试支持库文档

Why is JUnit 4 on Android not working?

Lint in Studio 2.3

Android — How to test a Service