前言
对于Android app来说,写起单元测试来瞻前顾后,一方面单元测试需要运行在模拟器上或者真机上,麻烦而且缓慢,另一方面,一些依赖Android SDK的对象(如Activity,TextView等)的测试非常头疼,Robolectric可以解决此类问题,它的设计思路便是通过实现一套JVM能运行的Android代码,从而做到脱离Android环境进行测试。本文对Robolectric3.0做了简单介绍,并列举了如何对Android的组件和常见功能进行测试的示例。
一、完整的一个测试类
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21,
shadows = {CustomShadowApplication.class,
CustomShadowOkHttpClient.class, CustomShadowXYJHttpUtils.class})
public class LoginActivityTest {
private LoginActivity loginActivity;
/**
* 执行初始化的操作
*
* @throws Exception
*/
@Before
public void setUp() throws Exception {
loginActivity = Robolectric.setupActivity(LoginActivity.class);
loginActivity.onCreate(null);
}
@After
public void tearDown() throws Exception {
CustomShadowXYJHttpUtils.reset();
}
@Test
public void should_show_message_when_account_is_empty() {
//given --准备条件
TextView userNameEditText = field("loginUsernameEdt").ofType(TextView.class).in(loginActivity).get();
userNameEditText.setText("");
//when --函数执行
TextView loginButton = (TextView) loginActivity.findViewById(R.id.login_button);
clickOn(loginButton);
//then -- 结果的返回值
assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo("请输入用户名");
}
}
代码覆盖率:
1>.语句覆盖:保证每一个语句都执行到了
2>.判定覆盖(分支覆盖):保证每一个分支都执行到
3>.条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)
4>.路径覆盖:保证每一个路径都覆盖到
代码测试覆盖率查看:
在Android Studio 开发工具中配置查看:
1.选中并运行编写的所有测试用例.
2、配置被测试对象
3.选中测试类--->点击Code Coverage--->点击加号添加被测试类--->完成
4.运行测试,选择Run 'Suites' with Coverage
5 Coverage Suites窗口会生成测试报告
6 下载测试报告到本地,选择绿色向上箭头选择路径下载
可以使用jacoco得到测试的代码覆盖率.
1.环境配置:
buildTypes {
debug {
testCoverageEnabled = true
}
}
2.在命令行执行,获得代码覆盖率的报告命令为createDebugCoverageReport
F:\Robolectric\Youdu_UnitTest>gradle clean createDebugCoverageReport
Observed package id 'build-tools;23.0.0-preview' in inconsistent location 'E:\tools\android-sdk\android-sdk\build-tools\23.0.0_rc2' (Expected 'E:\tools\android-sdk\android-sdk\build-tools\23.0.0-preview')
Observed package id 'build-tools;20.0.0' in inconsistent location 'E:\tools\android-sdk\android-sdk\build-tools\android-4.4W' (Expected 'E:\tools\android-sdk\android-sdk\build-tools\20.0.0')
Incremental java compilation is an incubating feature.
Download https://jcenter.bintray.com/org/jacoco/org.jacoco.agent/0.7.4.201502262128/org.jacoco.agent-0.7.4.201502262128.pom
Download https://jcenter.bintray.com/org/jacoco/org.jacoco.agent/0.7.4.201502262128/org.jacoco.agent-0.7.4.201502262128.jar
:clean
:app:clean
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:preReleaseBuild UP-TO-DATE
:app:prepareComAndroidSupportAnimatedVectorDrawable2511Library
:app:prepareComAndroidSupportAppcompatV72511Library
:app:prepareComAndroidSupportSupportCompat2511Library
:app:prepareComAndroidSupportSupportCoreUi2511Library
:app:prepareComAndroidSupportSupportCoreUtils2511Library
:app:prepareComAndroidSupportSupportFragment2511Library
:app:prepareComAndroidSupportSupportMediaCompat2511Library
:app:prepareComAndroidSupportSupportV42511Library
:app:prepareComAndroidSupportSupportVectorDrawable2511Library
:app:prepareDebugDependencies
:app:compileDebugAidl
:app:compileDebugRenderscript
:app:generateDebugBuildConfig
:app:generateDebugAssets UP-TO-DATE
:app:mergeDebugAssets
:app:generateDebugResValues UP-TO-DATE
:app:generateDebugResources
:app:mergeDebugResources
:app:processDebugManifest
:app:processDebugResources
:app:generateDebugSources
:app:compileDebugJavaWithJavac
:app:compileDebugNdk UP-TO-DATE
:app:compileDebugSources
:app:prePackageMarkerForDebug
:app:unzipJacocoAgent
:app:transformClassesWithJacocoForDebug
:app:transformClassesWithDexForDebug
:app:mergeDebugJniLibFolders
:app:transformNative_libsWithMergeJniLibsForDebug
:app:processDebugJavaRes UP-TO-DATE
:app:transformResourcesWithMergeJavaResForDebug
:app:validateDebugSigning
:app:packageDebug
:app:zipalignDebug
:app:assembleDebug
:app:preDebugAndroidTestBuild UP-TO-DATE
:app:prepareDebugAndroidTestDependencies
:app:compileDebugAndroidTestAidl
:app:processDebugAndroidTestManifest
:app:compileDebugAndroidTestRenderscript
:app:generateDebugAndroidTestBuildConfig
:app:generateDebugAndroidTestAssets UP-TO-DATE
:app:mergeDebugAndroidTestAssets
:app:generateDebugAndroidTestResValues UP-TO-DATE
:app:generateDebugAndroidTestResources
:app:mergeDebugAndroidTestResources
:app:processDebugAndroidTestResources
:app:generateDebugAndroidTestSources
:app:compileDebugAndroidTestJavaWithJavac
注: F:\Robolectric\Youdu_UnitTest\app\src\androidTest\java\xyj\com\youdu_unittest\ApplicationTest.java使用或覆盖了已过时的 API。
注: 有关详细信息, 请使用 -Xlint:deprecation 重新编译。
:app:compileDebugAndroidTestNdk UP-TO-DATE
:app:compileDebugAndroidTestSources
:app:prePackageMarkerForDebugAndroidTest
:app:transformClassesWithDexForDebugAndroidTest
:app:mergeDebugAndroidTestJniLibFolders
:app:transformNative_libsWithMergeJniLibsForDebugAndroidTest
:app:processDebugAndroidTestJavaRes UP-TO-DATE
:app:transformResourcesWithMergeJavaResForDebugAndroidTest
:app:packageDebugAndroidTest
:app:assembleDebugAndroidTest
:app:connectedDebugAndroidTest
:app:createDebugAndroidTestCoverageReport
Download https://jcenter.bintray.com/org/jacoco/org.jacoco.ant/0.7.4.201502262128/org.jacoco.ant-0.7.4.201502262128.pom
Download https://jcenter.bintray.com/org/jacoco/org.jacoco.report/0.7.4.201502262128/org.jacoco.report-0.7.4.201502262128.pom
Download https://jcenter.bintray.com/org/jacoco/org.jacoco.ant/0.7.4.201502262128/org.jacoco.ant-0.7.4.201502262128.jar
Download https://jcenter.bintray.com/org/jacoco/org.jacoco.report/0.7.4.201502262128/org.jacoco.report-0.7.4.201502262128.jar
:app:createDebugCoverageReport
BUILD SUCCESSFUL
Total time: 1 mins 3.45 secs
二、Shadow的使用
Shadow是Robolectric的立足之本,如其名,作为影子,一定是变幻莫测,时有时无,且依存于本尊。因此,框架针对Android SDK中的对象,提供了很多影子对象(如Activity和ShadowActivity、TextView和ShadowTextView等),这些影子对象,丰富了本尊的行为,能更方便的对Android相关的对象进行测试。
上述的实例中有如:CustomShadowOkHttpClient,CustomShadowXYJHttpUtils等类,这些类是为了模拟那些不好编写测试用例而作为一个影子,提供方便我们用于模拟业务场景进行测试的api。
- 使用框架提供的Shadow对象
@Test
public void testActivityShadow() {
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
TextView textView = field("textView").ofType(TextView.class).in(mainActivity).get();
Intent expectedIntent = new Intent(mainActivity, LoginActivity.class);
clickOn(textView);
//通过Shadows.shadowOf()可以获取很多Android对象的Shadow对象
ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);
ShadowApplication shadowApplication = Shadows.shadowOf(RuntimeEnvironment.application);
assertThat(shadowActivity.getNextStartedActivity()).isEqualTo(expectedIntent);
assertThat(shadowApplication.getNextStartedActivity()).isNull();
}
- 如何自定义Shadow对象
首先,创建原始对象UserInfo
public class UserInfo {
private String userName;
private String password;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
其次,创建UserInfo 的Shadow对象
@Implements(UserInfo.class)
public class ShadowUserInfo {
@Implementation
public String getPassword() {
return "123456";
}
@Implementation
public String getUserName() {
return "admin";
}
}
接下来,需自定义TestRunner,添加UserInfo对象为要进行Shadow的对象
public class YouduTestRunner extends RobolectricGradleTestRunner {
public YouduTestRunner(Class<?> klass) throws InitializationError {
super(klass);
}
@Override
public InstrumentationConfiguration createClassLoaderConfig() {
InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder();
/**
* 添加要进行Shadow的对象
*/
builder.addInstrumentedClass(UserInfo.class.getName());
return builder.build();
}
// @Override
// protected AndroidManifest getAppManifest(Config config) {
// String manifestPath = BUILD_OUTPUT + "manifests/full/debug/AndroidManifest.xml";
// String resDir = BUILD_OUTPUT + "res/merged/debug";
// String assetsDir = BUILD_OUTPUT + "assets/debug";
//
// AndroidManifest manifest = createAppManifest(Fs.fileFromPath(manifestPath),
// Fs.fileFromPath(resDir),
// Fs.fileFromPath(assetsDir),"com.uthing");
// return manifest;
// }
最后,在测试用例中,ShadowPerson对象将自动代替原始对象,调用Shadow对象的数据和行为
@RunWith(YouduTestRunner.class)
@Config(constants = BuildConfig.class,
sdk = 21, shadows = {ShadowUserInfo.class})
public class ShadowTest {
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testCustomShadow() throws Exception {
UserInfo userInfo = new UserInfo();
//getName()实际上调用的是ShadowPerson的方法
assertThat(userInfo.getUserName()).isEqualTo("admin");
//获取userInfo对象对应的Shadow对象
ShadowUserInfo shadowPerson = (ShadowUserInfo) ShadowExtractor.extract(userInfo);
assertThat("123456").isEqualTo(shadowPerson.getPassword());
}
}
以上就是shadow一个对象的完成过程。在业务逻辑中可根据具体场景来shadow来模拟想要的数据,编写相应的测试用例。
三、Mockito 的使用
所谓的mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:
- 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
- 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作
要使用Mock,一般需要用到mock框架,我们使用 Mockito 这个框架,这个是Java中使用最广泛的一个mock框架。
例如:
Mock一个List类型的对象实例,可以采用如下方式:
List list = mock(List.class); //mock得到一个对象,也可以用@mock注入一个对象
所得到的list对象实例便是List类型的实例,如果不采用mock,List其实只是个接口,我们需要构造或者借助ArrayList才能进行实例化。与Shadow不同,Mock构造的是一个虚拟的对象,用于解耦真实对象所需要的依赖。Mock得到的对象仅仅是具备测试对象的类型,并不是真实的对象,也就是并没有执行过真实对象的逻辑。
四、测试实例
- 创建Activity实例
@Test
public void testActivity() {
MainActivity sampleActivity = Robolectric.setupActivity(MainActivity.class);
assertNotNull(sampleActivity);
assertEquals(sampleActivity.getTitle(), "首页");
}
- 生命周期
@Test
public void testLifecycle() {
ActivityController<SampleActivity> activityController = Robolectric.buildActivity(SampleActivity.class).create().start();
Activity activity = activityController.get();
TextView textview = (TextView) activity.findViewById(R.id.tv_lifecycle_value);
assertEquals("onCreate",textview.getText().toString());
activityController.resume();
assertEquals("onResume", textview.getText().toString());
activityController.destroy();
assertEquals("onDestroy", textview.getText().toString());
}
3.UI组件状态
@Test
public void should_update_ui_when_click_login_button() {
//given
CheckBox checkBox = (CheckBox) loginActivity.findViewById(R.id.remember_passWord_checkbox);
Button userNameButton = field("loginButton").ofType(Button.class).in(loginActivity).get();
EditText userNameEditText = field("userNameEditText").ofType(EditText.class).in(loginActivity).get();
EditText passwordEditText = field("passwrodEditText").ofType(EditText.class).in(loginActivity).get();
//when --函数执行
userNameEditText.setText("admin");
passwordEditText.setText("123");
assertTrue(userNameButton.isEnabled());
checkBox.setChecked(true);
//then -- 结果的返回值
clickOn(checkBox);
assertThat(checkBox.isChecked()).isFalse();
userNameButton.performClick();
assertThat(checkBox.isChecked()).isTrue();
}
4.跳转
@Test
public void testStartActivity() {
Button nextButton = (Button) sampleActivity.findViewById(R.id.main_button);
nextButton.performClick(); //按钮点击后跳转到下一个Activity
Intent expectedIntent = new Intent(sampleActivity, LoginActivity.class);
Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();
assertEquals(expectedIntent, actualIntent);
}
5.Dialog
@Test
public void testDialog(){
dialogBtn.performClick();
AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
assertNotNull(latestAlertDialog);
}
6.Toast
@Test
public void should_show_message_when_account_is_empty() {
//given --准备条件
EditText userNameEditText = field("userNameEditText").ofType(EditText.class).in(loginActivity).get();
userNameEditText.setText("");
//when --函数执行
TextView loginButton = (TextView) loginActivity.findViewById(R.id.btn_login);
clickOn(loginButton);
//then -- 结果的返回值
assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo("请输入用户名");
}
7.Fragment的测试
Fragment是Activity的一部分,在Robolectric模拟执行Activity过程中,如果触发了被测试的代码中的Fragment添加逻辑,Fragment会被添加到Activity中。需要注意Fragment出现的时机,如果目标Activity中的Fragment的添加是执行在onResume阶段,在Activity被Robolectric执行resume()阶段前,该Activity中并不会出现该Fragment。采用Robolectric主动添加Fragment的方法如下:
@Test
public void addfragment(Activity activity, int fragmentContent){
FragmentTestUtil.startFragment(activity.getSupportFragmentManager().findFragmentById(fragmentContent));
Fragment fragment = activity.getSupportFragmentManager().findFragmentById(fragmentContent);
assertNotNull(fragment);
}
startFragment()函数的主体便是常用的添加fragment的代码。切换一个Fragment往往由Activity中的代码逻辑完成,需要Activity的引用。
总结
单元测试并不是一个能直接产生回报的工程,它的运行以及覆盖率也不能直接提升代码质量,但其带来的代码控制力能够大幅度降低大规模协同开发的风险。现在的商业App开发都是大型团队协作开发,不断会有新人加入,无论新人是刚入行的应届生还是工作多年,在代码存在一定业务耦合度的时候,修改代码就有一定风险,可能会影响之前比较隐蔽的业务逻辑,或者是丢失曾经的补丁,如果有高覆盖率的单元测试工程,就能很快定位到新增代码对现有项目的影响,与QA验收不同,这种影响是代码级的。