Android单元测试之Local unit tests(下)
-
- Android单元测试之Local unit tests(下)
- 前言
- Robolectric
- 添加依赖
- 测试例子-Robolectric基本使用
- 无处不在的 shadowXXX 是什么?
- 测试例子-Robolectric使用进阶
- @Config(qualifiers =”“) 配置
- 测试例子-Activity的生命周期
- 自定义Shdow类
- 附加模块
- JUnit 4 & Mockito & Robolectric
- Robolectric
- 参考链接
- 前言
- Android单元测试之Local unit tests(下)
Android单元测试之Local unit tests(下)
前言
接上篇 Android单元测试之Local unit tests(上)
前面介绍了这么多,接下来我们要介绍神器“Robolectric”就是为Android而生的单元测试框架
Robolectric
我们在前文介绍过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基本使用
我们通过测试例子来学习下如何使用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:
<?xml version="1.0" encoding="utf-8"?><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
,则要进行以下操作:
按图片配置Working directory
即可。
我们运行完此次的单元测试后,控制台会输出”onClick”,验证了按钮点击事件的相关逻辑。
无处不在的 shadowXXX 是什么?
你可能注意到了例子中我们配置了一个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类。
测试例子-Robolectric使用进阶
我们在刚刚的例子只验证了一个按钮的点击事件,还有另外两个按钮的点击事件我们来进行测试:
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等进行测试
@Config(qualifiers =”“) 配置
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的生命周期
我们都知道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 happenedactivityController.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()
的调用。
自定义Shdow类
除了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"}
JUnit 4 & Mockito & Robolectric
我们以上介绍了这么多用于单元测试的框架,各有优缺点。实际应用中更是结合起来应用才会给开发者带来最大的效率。
1. 对pure java类或工具类方法测试使用JUnit 4。
2. 验证方法是否调用/流程对测试结果影响可以使用Mockito。
2. 涉及Android view及四大组件测试用Robolectric。
参考链接
Fundamentals of Testing
Android单元测试之JUnit4
robolectric
使用Robolectric进行Android单元测试
Android 集成Robolectric下的一些坑
Robolectric使用教程
更多相关文章
- Android(安卓)Pitfall - Fragment.startActivityForResult(), re
- android笔记
- 让TextView 自带滚动条
- 不可或缺 Windows Native (25) - C++: windows app native, andr
- Ted Mosby - 一个MVP框架的软件架构
- android开发每日汇总【2011-11-26】
- Android(安卓)Market google play store帐号注册方法流程 及发布
- 不可或缺 Windows Native (25) - C++: windows app native, andr
- Android面试题精选:讲一讲 Android(安卓)的事件分发机制