前言
對于Android app來說,寫起單元測試來瞻前顧后收壕,一方面單元測試需要運(yùn)行在模擬器上或者真機(jī)上歼捐,麻煩而且緩慢,另一方面醉者,一些依賴Android SDK的對象(如Activity但狭,TextView等)的測試非常頭疼披诗,Robolectric可以解決此類問題,它的設(shè)計思路便是通過實(shí)現(xiàn)一套JVM能運(yùn)行的Android代碼立磁,從而做到脫離Android環(huán)境進(jìn)行測試呈队。本文對Robolectric3.0做了簡單介紹,并列舉了如何對Android的組件和常見功能進(jìn)行測試的示例唱歧。
一宪摧、完整的一個測試類
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21,
shadows = {CustomShadowApplication.class,
CustomShadowOkHttpClient.class, CustomShadowXYJHttpUtils.class})
public class LoginActivityTest {
private LoginActivity loginActivity;
/**
* 執(zhí)行初始化的操作
*
* @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 --準(zhǔn)備條件
TextView userNameEditText = field("loginUsernameEdt").ofType(TextView.class).in(loginActivity).get();
userNameEditText.setText("");
//when --函數(shù)執(zhí)行
TextView loginButton = (TextView) loginActivity.findViewById(R.id.login_button);
clickOn(loginButton);
//then -- 結(jié)果的返回值
assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo("請輸入用戶名");
}
}
代碼覆蓋率:
1>.語句覆蓋:保證每一個語句都執(zhí)行到了
2>.判定覆蓋(分支覆蓋):保證每一個分支都執(zhí)行到
3>.條件覆蓋:保證每一個條件都覆蓋到true和false(即if、while中的條件語句)
4>.路徑覆蓋:保證每一個路徑都覆蓋到
代碼測試覆蓋率查看:
在Android Studio 開發(fā)工具中配置查看:
1.選中并運(yùn)行編寫的所有測試用例.
2颅崩、配置被測試對象
3.選中測試類--->點(diǎn)擊Code Coverage--->點(diǎn)擊加號添加被測試類--->完成
4.運(yùn)行測試,選擇Run 'Suites' with Coverage
5 Coverage Suites窗口會生成測試報告
6 下載測試報告到本地,選擇綠色向上箭頭選擇路徑下載
可以使用jacoco得到測試的代碼覆蓋率.
1.環(huán)境配置:
buildTypes {
debug {
testCoverageEnabled = true
}
}
2.在命令行執(zhí)行几于,獲得代碼覆蓋率的報告命令為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。
注: 有關(guān)詳細(xì)信息, 請使用 -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相關(guān)的對象進(jìn)行測試。
上述的實(shí)例中有如:CustomShadowOkHttpClient恨锚,CustomShadowXYJHttpUtils等類宇驾,這些類是為了模擬那些不好編寫測試用例而作為一個影子,提供方便我們用于模擬業(yè)務(wù)場景進(jìn)行測試的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對象
首先课舍,創(chuàng)建原始對象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;
}
}
其次,創(chuàng)建UserInfo 的Shadow對象
@Implements(UserInfo.class)
public class ShadowUserInfo {
@Implementation
public String getPassword() {
return "123456";
}
@Implementation
public String getUserName() {
return "admin";
}
}
接下來他挎,需自定義TestRunner筝尾,添加UserInfo對象為要進(jìn)行Shadow的對象
public class YouduTestRunner extends RobolectricGradleTestRunner {
public YouduTestRunner(Class<?> klass) throws InitializationError {
super(klass);
}
@Override
public InstrumentationConfiguration createClassLoaderConfig() {
InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder();
/**
* 添加要進(jìn)行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對象將自動代替原始對象筹淫,調(diào)用Shadow對象的數(shù)據(jù)和行為
@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()實(shí)際上調(diào)用的是ShadowPerson的方法
assertThat(userInfo.getUserName()).isEqualTo("admin");
//獲取userInfo對象對應(yīng)的Shadow對象
ShadowUserInfo shadowPerson = (ShadowUserInfo) ShadowExtractor.extract(userInfo);
assertThat("123456").isEqualTo(shadowPerson.getPassword());
}
}
以上就是shadow一個對象的完成過程。在業(yè)務(wù)邏輯中可根據(jù)具體場景來shadow來模擬想要的數(shù)據(jù)呢撞,編寫相應(yīng)的測試用例损姜。
三饰剥、Mockito 的使用
所謂的mock就是創(chuàng)建一個類的虛假的對象,在測試環(huán)境中摧阅,用來替換掉真實(shí)的對象汰蓉,以達(dá)到兩大目的:
- 驗(yàn)證這個對象的某些方法的調(diào)用情況,調(diào)用了多少次棒卷,參數(shù)是什么等等
- 指定這個對象的某些方法的行為顾孽,返回特定的值,或者是執(zhí)行特定的動作
要使用Mock比规,一般需要用到mock框架若厚,我們使用 Mockito 這個框架,這個是Java中使用最廣泛的一個mock框架蜒什。
例如:
Mock一個List類型的對象實(shí)例测秸,可以采用如下方式:
List list = mock(List.class); //mock得到一個對象,也可以用@mock注入一個對象
所得到的list對象實(shí)例便是List類型的實(shí)例吃谣,如果不采用mock乞封,List其實(shí)只是個接口,我們需要構(gòu)造或者借助ArrayList才能進(jìn)行實(shí)例化岗憋。與Shadow不同,Mock構(gòu)造的是一個虛擬的對象锚贱,用于解耦真實(shí)對象所需要的依賴仔戈。Mock得到的對象僅僅是具備測試對象的類型,并不是真實(shí)的對象拧廊,也就是并沒有執(zhí)行過真實(shí)對象的邏輯监徘。
四、測試實(shí)例
- 創(chuàng)建Activity實(shí)例
@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組件狀態(tài)
@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 --函數(shù)執(zhí)行
userNameEditText.setText("admin");
passwordEditText.setText("123");
assertTrue(userNameButton.isEnabled());
checkBox.setChecked(true);
//then -- 結(jié)果的返回值
clickOn(checkBox);
assertThat(checkBox.isChecked()).isFalse();
userNameButton.performClick();
assertThat(checkBox.isChecked()).isTrue();
}
4.跳轉(zhuǎn)
@Test
public void testStartActivity() {
Button nextButton = (Button) sampleActivity.findViewById(R.id.main_button);
nextButton.performClick(); //按鈕點(diǎn)擊后跳轉(zhuǎn)到下一個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 --準(zhǔn)備條件
EditText userNameEditText = field("userNameEditText").ofType(EditText.class).in(loginActivity).get();
userNameEditText.setText("");
//when --函數(shù)執(zhí)行
TextView loginButton = (TextView) loginActivity.findViewById(R.id.btn_login);
clickOn(loginButton);
//then -- 結(jié)果的返回值
assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo("請輸入用戶名");
}
7.Fragment的測試
Fragment是Activity的一部分吧碾,在Robolectric模擬執(zhí)行Activity過程中凰盔,如果觸發(fā)了被測試的代碼中的Fragment添加邏輯,F(xiàn)ragment會被添加到Activity中倦春。需要注意Fragment出現(xiàn)的時機(jī)户敬,如果目標(biāo)Activity中的Fragment的添加是執(zhí)行在onResume階段,在Activity被Robolectric執(zhí)行resume()階段前睁本,該Activity中并不會出現(xiàn)該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()函數(shù)的主體便是常用的添加fragment的代碼。切換一個Fragment往往由Activity中的代碼邏輯完成呢堰,需要Activity的引用抄瑟。
總結(jié)
單元測試并不是一個能直接產(chǎn)生回報的工程,它的運(yùn)行以及覆蓋率也不能直接提升代碼質(zhì)量枉疼,但其帶來的代碼控制力能夠大幅度降低大規(guī)模協(xié)同開發(fā)的風(fēng)險∑ぜ伲現(xiàn)在的商業(yè)App開發(fā)都是大型團(tuán)隊協(xié)作開發(fā)鞋拟,不斷會有新人加入,無論新人是剛?cè)胄械膽?yīng)屆生還是工作多年惹资,在代碼存在一定業(yè)務(wù)耦合度的時候贺纲,修改代碼就有一定風(fēng)險,可能會影響之前比較隱蔽的業(yè)務(wù)邏輯布轿,或者是丟失曾經(jīng)的補(bǔ)丁哮笆,如果有高覆蓋率的單元測試工程,就能很快定位到新增代碼對現(xiàn)有項目的影響汰扭,與QA驗(yàn)收不同稠肘,這種影響是代碼級的。