接上篇 Android单元测试之Local unit tests(上)
前面介绍了这么多,接下来我们要介绍神器“Robolectric”就是为Android而生的单元测试框架
我们在前文介绍过Android中做本地单元测试是无法调用android.jar真正实现方法的,之前的解决方案是通过Mockito这类Mock框架来模拟调用。
而Robolectric 单元测试框架,通过在Android SDK类被加载时重写它们,并使它们能够在常规的JVM上运行成为可能。测试可以在几秒钟内在JVM上运行完成。
Robolectric与Mockito相比,使用上更接近“黑盒测试”,使得测试更有效地进行重构,并且无需Mock大量Android的方法,专注于应用程序的测试。当然它们也可以一起使用。
dependencies {
testCompile 'junit:junit:4.12'
testCompile "org.robolectric:robolectric:3.8"
}
我们通过测试例子来学习下如何使用Robolectric,我们新建个普通的Activity,比较简单,代码如下:
public class MyActivity extends Activity {
private static final String TAG = "MyActivity";
@Override
protected String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
init();
}
private void init() {
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "onClick: ");
}
});
findViewById(R.id.btn_go).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setClass(getApplication(), MainActivity.class);
startActivity(intent);
}
});
findViewById(R.id.btn_test_handler).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Toast.makeText(MyActivity.this, "hello", Toast.LENGTH_SHORT).show();
}
},1000);
}
});
}
}
布局文件 activity_my.xml:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.licheng.testapp.test.MyActivity">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="log"/>
<Button
android:id="@+id/btn_go"
android:layout_below="@id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="start new activity"/>
<Button
android:id="@+id/btn_test_handler"
android:layout_below="@id/btn_go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="post delay msg"/>
RelativeLayout>
很简单,放了两个按钮分别注册了点击事件监听。
我们现在对这个MyActivity
进行单元测试,在类名上CTRL+SHIFT+T,生成相应的测试类MyActivityTest
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, shadows = {ShadowLog.class}, sdk = 23)
public class MyActivityTest {
@Before
public void setUp() throws Exception {
// 设置log输出模式,配置后控制台可见日志
ShadowLog.stream = System.out;
}
@Test
public void clickingButton_shouldPrintLog() throws Exception {
MyActivity activity = Robolectric.setupActivity(MyActivity.class);
Button btn = (Button) activity.findViewById(R.id.btn);
// 控制台打印出"onClick: ",验证按钮点击事件成功
btn.performClick();
}
}
@RunWith(RobolectricTestRunner.class)
@Config()
用于配置测试环境,可以对类或者方法进行设置。配置参数很多,常见的有:sdk,manifest,qualifiers。基本配置完成后,我们就可以开始运行测试了。首次运行时间较长,因为Robolectric会远程下载相应sdk版本jar包。(如长时间无反应,建议重试)
注意:
如果运行过程爆出java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml
,则要进行以下操作:
我们运行完此次的单元测试后,控制台会输出”onClick”,验证了按钮点击事件的相关逻辑。
你可能注意到了例子中我们配置了一个shadows = {ShadowLog.class}
,这个ShadowLog是什么呢?
其实ShadowLog是Robolectric为JVM重写的android sdk的android.util.Log.class
,Robolectric中有大量的ShadowXXX类,当一个Android OS类被实例化,Robolectric会搜索相应的Shadow类;如果找到了,将创建与之关联的Shadow对象。Robolectirc确保:如果存在,Shadow类中的相应方法先被调用,这样就有机会做测试相关逻辑。
常见的有:ShadowActivity,ShadowViewGroup,ShadowView……等等无需配置会自动替换。使用者也可以自定义Shadow类。
我们在刚刚的例子只验证了一个按钮的点击事件,还有另外两个按钮的点击事件我们来进行测试:
public Context getContext() {
return RuntimeEnvironment.application;
}
@Test
public void clickBtn_shouldStartNewActivity() throws Exception {
Button button = (Button) activity.findViewById(R.id.btn_go);
// 验证按钮点击跳转Activity
button.performClick();
// 对Activity进行Shadow
ShadowActivity shadowActivity = Shadow.extract(activity);
// 获取实际跳转intent
String actualIntentName = shadowActivity.getNextStartedActivity().getComponent().getClassName();
// 构造预期跳转intent
Intent expectedIntent = new Intent(getContext(), MainActivity.class);
String expectedIntentName = expectedIntent.getComponent().getClassName();
// 验证实际跳转与预期跳转是否一致,比对跳转Activity类名
Assert.assertThat(expectedIntentName, Matchers.is(actualIntentName));
}
@Test
public void clickBtn_shouldPostDelayMsg() {
MyActivity activity = Robolectric.setupActivity(MyActivity.class);
Button button = (Button) activity.findViewById(R.id.btn_test_handler);
// 验证按钮点击跳转Activity
button.performClick();
/**
* java.lang.AssertionError:
* Expected: is "hello"
* but: was null
*
* 测试不通过,因为是延迟发送消息
*/
// Assert.assertThat(ShadowToast.getTextOfLatestToast(), Matchers.is("hello"));
// 通过ShadowLooper 完成所有task的执行,包括延迟task
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertThat(ShadowToast.getTextOfLatestToast(), Matchers.is("hello"));
}
结论:
1. 可以通过RuntimeEnvironment.application
方式获取Context
2. 通过对Activity进行Shadow,可以调用getNextStartedActivity()来验证跳转。
3. 通过ShadowLooper.runUiThreadTasksIncludingDelayedTasks();完成所有task的执行,包括延迟task
Robolectric可以类似的对Fragment,Service,Broadcast,Application等进行测试
qualifiers配置可以模拟真实机器运行环境,包括语言,地区,屏幕,横竖屏等,之前我们使用过@Config但一直没用过这个特性,接下来我们介绍下:
@Test @Config(qualifiers = "fr-rFR-w360dp-h640dp-xhdpi")
public void testItOnFrenchNexus5() { ... }
Robolectric测试会通过使用Android资源限定符解析规则,自动选择正确的资源:
values/strings.xml
<string name="not_overridden">Not Overriddenstring>
<string name="overridden">Unqualified valuestring>
<string name="overridden_twice">Unqualified valuestring>
values-en/strings.xml
<string name="overridden">English qualified valuestring>
<string name="overridden_twice">English qualified valuestring>
values-en-port/strings.xml
<string name="overridden_twice">English portrait qualified valuestring>
@Test
@Config(qualifiers="en-port")
public void shouldUseEnglishAndPortraitResources() {
final Context context = RuntimeEnvironment.application;
assertThat(context.getString(R.id.not_overridden)).isEqualTo("Not Overridden");
assertThat(context.getString(R.id.overridden)).isEqualTo("English qualified value");
assertThat(context.getString(R.id.overridden_twice)).isEqualTo("English portrait qualified value");
}
更多支持的配置请前往 Configuring Robolectric .
我们都知道Activity相比于普通java类最大的特点就是生命周期,正常的生命周期回调是由AMS完成的。单元测试肯定不会有AMS,而我们很多情况是需要对生命周期方法进行测试的,Robolectric提供了相应的解决方案。
// 使用Robolectric.buildActivity() 能够完成一个Activity的构建,并会返回ActivityController对象操作生命周期方法
ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create();
比如我们想验证onResume调用的影响
ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start();
Activity activity = controller.get();
// assert that something hasn't happened
activityController.resume();
// assert it happened!
还有很多相似的生命周期回调方法
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().start().resume().visible().get();
当你不关心生命周期时,通过Robolectric.setupActivity
构建Activity,这会帮我们完成了一次完整的生命周期调用
public static T setupActivity(Class activityClass) {
return buildActivity(activityClass).setup().get();
}
public ActivityController setup() {
return create().start().postCreate(null).resume().visible();
}
注意:
我们知道在真正的Android应用程序中,onCreate之后的某个时机Activity的view hierarchy才会attach到Window完成可见。Robolectric将Activity的可见时机交给测试人员去决定。当你需要进行交互前应该完成visible()
的调用。
除了Robolectric中大量的ShdowXXX类外,我们还可以自定义Shdow类替换和丰富原有方法,帮助我们完成测试。自定义Shadow类规则如下:
- Shadow类须要一个public的无參构造方法以方便Robolectric框架能够实例化它。通过@Implements注解与原始类关联在一起
- 若原始类有有參构造方法。在Shadow类中定义public void类型的名为constructor的方法,且方法參数与原始类的构造方法參数一致
- 定义与原始类方法签名一致的方法,在里面重写实现,Shadow方法需用@Implementation进行注解
原始类:
public class LoginApi {
public Context mContext;
public LoginApi(Context context) {
mContext = context;
}
public void login(String name, String password, LoginCallback callback) {
if (name.equals("lee") && password.equals("123")) {
callback.onSuccess();
} else {
callback.onFailed();
}
}
}
Shadow类:
@Implements(LoginApi.class)
public class ShadowLoginApi {
/**
* 通过@RealObject注解能够訪问原始对象,但注意,通过@RealObject注解的变量调用方法,依旧会调用Shadow类的方法。而不是原始类的方法
* 仅仅能用来訪问原始类的field
*/
@RealObject
LoginApi mLoginApi;
/**
* 须要一个无參构造方法
*/
public ShadowLoginApi() {
}
/**
* 相应原始类的构造方法
*
* @param context 相应原始类构造方法的传入參数
*/
public void __constructor__(Context context) {
mLoginApi.mContext = context;
}
/**
* 原始对象的方法被调用的时候,Robolectric会依据方法签名查找相应的Shadow方法并调用
*/
@Implementation
public void login(String name, String password, LoginCallback callback){
// 直接调用成功,替换原有逻辑
callback.onSuccess();
}
}
我们验证下登录:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class,shadows = {ShadowLoginApi.class}, sdk = 23)
public class LoginActivityTest {
@Before
public void setUp() throws Exception {
}
@Test
public void login() throws Exception {
LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class);
EditText etAccount = (EditText) loginActivity.findViewById(R.id.et_account);
etAccount.setText("lee");
EditText etPassword = (EditText) loginActivity.findViewById(R.id.et_password);
etPassword.setText("1234");
loginActivity.findViewById(R.id.btn_login).performClick();
/**
* 验证成功,LoginActivity中对LoginApi.class的调用被替换为ShadowLoginApi.class的方法,默认成功
* 我们可以通过修改ShadowLoginApi.class的方法验证不同流程
*/
Assert.assertThat(ShadowToast.getTextOfLatestToast(), Matchers.equalTo("success!"));
}
}
我们在测试环境配置了shadows = {ShadowLoginApi.class}
,单元测试时ShadowLoginApi会覆盖LoginApi.class中方法,其实这里有点像我们上一节介绍的Mockito,但是有个好处就是静态方法,final方法都能进行覆盖。(可以避免mockito和powermockito混合使用)
为了减少被测试应用程序的外部依赖数量,Robolectric的Shdow被分成各种附加包
,主Robolectric模块仅提供基础Android SDK中提供的类的Shdow类。附加模块提供了诸如appcompat或支持库之类的其他Shdow类。
SDK Package | Robolectric Add-On Package |
---|---|
com.android.support.support-v4 | org.robolectric:shadows-supportv4 |
com.android.support.multidex | org.robolectric:shadows-multidex |
com.google.android.gms:play-services | org.robolectric:shadows-playservices |
org.apache.httpcomponents:httpclient | org.robolectric:shadows-httpclient |
dependencies {
......
testCompile "org.robolectric:shadows-supportv4:3.8"
testCompile "org.robolectric:shadows-multidex:3.8"
testCompile "org.robolectric:shadows-playservices"
testCompile "org.robolectric:shadows-httpclient:3.8"
}
我们以上介绍了这么多用于单元测试的框架,各有优缺点。实际应用中更是结合起来应用才会给开发者带来最大的效率。
1. 对pure java类或工具类方法测试使用JUnit 4。
2. 验证方法是否调用/流程对测试结果影响可以使用Mockito。
2. 涉及Android view及四大组件测试用Robolectric。
Fundamentals of Testing
Android单元测试之JUnit4
robolectric
使用Robolectric进行Android单元测试
Android 集成Robolectric下的一些坑
Robolectric使用教程