技术点
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,子类)被访问。 比如只在测试中有效的方法,可以这样写。
@RestrictScope(TESTS)
public abstract int getUserId();
遗憾的是,目前这个注解只起到提示作用,即便不在限定范围内访问,也不会发生什么。不过即将到来的AndroidStudio 2.3 版本中这方面会加强,在Lint的时候会提出警告。期待期待~
AndroidJUnitRunner
设备测试本身不支持JUnit4。AndroidJUnitRunner 类是一个 JUnit 测试运行器,可让您在 Android 设备上运行 JUnit 3 或 JUnit 4 样式测试类。
-
添加依赖
dependencies { compile 'com.android.support:support-annotations:22.2.0' androidTestCompile 'com.android.support.test:runner:0.4.1' } -
指定 testInstrumentationRunner:
android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } } -
给测试类加上注解 :
@RunWith(AndroidJUnit4.class) -
然后就可以写Junit4测试代码了。
实战
比如项目中的一个模块( 比如下面的 TheSDK ),初始化完成的时候,应该要启动了某个Service。这个逻辑要怎么测试?
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做一点改造,具体看注释:
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,写测试断言,搞定!:
@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中,就不用重复手写了)
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());
}
}
参考资料与扩展阅读