android Robolectric 運(yùn)用實(shí)踐

前言

對于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)行編寫的所有測試用例.

步驟1.jpg

2颅崩、配置被測試對象

操作2.png
操作3.png

3.選中測試類--->點(diǎn)擊Code Coverage--->點(diǎn)擊加號添加被測試類--->完成

4.運(yùn)行測試,選擇Run 'Suites' with Coverage

操作4.png

5 Coverage Suites窗口會生成測試報告

操作5.png

6 下載測試報告到本地,選擇綠色向上箭頭選擇路徑下載

操作6.png

操作7.png

可以使用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猴伶。

  1. 使用框架提供的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();

    }
  1. 如何自定義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á)到兩大目的:

  1. 驗(yàn)證這個對象的某些方法的調(diào)用情況,調(diào)用了多少次棒卷,參數(shù)是什么等等
  2. 指定這個對象的某些方法的行為顾孽,返回特定的值,或者是執(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í)例

  1. 創(chuàng)建Activity實(shí)例
  @Test
    public void testActivity() {
        MainActivity sampleActivity = Robolectric.setupActivity(MainActivity.class);
        assertNotNull(sampleActivity);
        assertEquals(sampleActivity.getTitle(), "首頁");
    }
  1. 生命周期
@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)收不同稠肘,這種影響是代碼級的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末萝毛,一起剝皮案震驚了整個濱河市项阴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌笆包,老刑警劉巖环揽,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異庵佣,居然都是意外死亡歉胶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門巴粪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來通今,“玉大人,你說我怎么就攤上這事肛根”杷” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵派哲,是天一觀的道長臼氨。 經(jīng)常有香客問我,道長芭届,這世上最難降的妖魔是什么储矩? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮喉脖,結(jié)果婚禮上椰苟,老公的妹妹穿的比我還像新娘。我一直安慰自己树叽,他們只是感情好舆蝴,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般洁仗。 火紅的嫁衣襯著肌膚如雪层皱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天赠潦,我揣著相機(jī)與錄音叫胖,去河邊找鬼。 笑死她奥,一個胖子當(dāng)著我的面吹牛瓮增,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哩俭,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼绷跑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凡资?” 一聲冷哼從身側(cè)響起砸捏,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎隙赁,沒想到半個月后垦藏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伞访,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年掂骏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厚掷。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡芭挽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蝗肪,到底是詐尸還是另有隱情,我是刑警寧澤蠕趁,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布薛闪,位于F島的核電站,受9級特大地震影響俺陋,放射性物質(zhì)發(fā)生泄漏豁延。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一腊状、第九天 我趴在偏房一處隱蔽的房頂上張望诱咏。 院中可真熱鬧,春花似錦缴挖、人聲如沸袋狞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苟鸯。三九已至同蜻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間早处,已是汗流浹背湾蔓。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留砌梆,地道東北人默责。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像咸包,于是被迫代替她去往敵國和親桃序。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 一.基本介紹 背景: 目前處于高速迭代開發(fā)中的Android項目往往需要除黑盒測試外更加可靠的質(zhì)量保障诉儒,這正是單元...
    anmi7閱讀 2,016評論 0 6
  • Android單元測試介紹 處于高速迭代開發(fā)中的Android項目往往需要除黑盒測試外更加可靠的質(zhì)量保障葡缰,這正是單...
    東經(jīng)315度閱讀 3,093評論 6 37
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,527評論 25 707
  • afinalAfinal是一個android的ioc,orm框架 https://github.com/yangf...
    passiontim閱讀 15,401評論 2 45
  • 我是日記星球226號星寶寶婷婷忱反,我在參加日記星球第五期的21天蛻變之旅泛释,這是我在日記星球?qū)懙牡?1篇原創(chuàng)日記。...
    天鳴老師閱讀 746評論 2 3