單元測試匯總
轉自:https://blog.csdn.net/u012933335/rss/list
[原]Android單測調研篇 |
1. 為什麽做單測單測的好處
不得不寫單測的原因
難處難於堅持,在快速叠代開發過程中,可供寫單測的時間過少 擴展TDD(Test Drive Develop):測試驅動開發,是一種非常高效的開發方式 2. 測試框架2.1 概述
2.2 junit4.12Junit測試是程序員測試,即所謂白盒測試,因為程序員知道被測試的軟件如何(How)完成功能和完成什麽樣(What)的功能。Junit是一套框架,繼承TestCase類,就可以用Junit進行自動測試了。 示例代碼: @Before: 執行單測之前的初始化操作。 @After:單測完成後收尾工作。 @Before public void setUp() throws Exception { } @After public void tearDown() throws Exception { } assume: 用於判斷測試用例的入參是否有業務含義的工具,如果入參不符合預期時會拋出 assumptionViolatedException、assumeTrue/assumeFalse、 assumeNotNull、 assumeThat、 assumeNoException @RunWith(Theories.class) public class AssumeTest { @DataPoints public static String[] names = {"LiLei", "HanMeiMei"}; @DataPoints public static int[] ages = {10, -2, 12}; @Theory public void printAge(String name, int age) { Assume.assumeTrue(age > 0); System.out.println(String.format("%s‘s Name is %s.", name, age)); } } assert :用於常用的測試結果驗證 AssertTrue、AssertFalse:結果的true、false。 AssertThat:使用Matcher做自定義的校驗。 AssertEquals、AssertNotEquals:判斷兩個對象是否相等。 AssertNull、AssertNotNull:判斷對象是否為空。 AssertSame:判斷兩個對象是否為同一個,不同於equals這裏是使用“==”判斷。 AssertArrayEquals:判斷兩個數組是否相等。 @Test public void sum() throws Exception { assertEquals(mCalculator.sum(3, 4), 7); } verify : 主要用於驗證方法是否執行 @Test public void testVerify() { List mockedList = mock(List.class); mockedList.add("one"); mockedList.clear(); mockedList.add("3"); // verification verify(mockedList).add("one"); verify(mockedList).clear(); } 其他高級用法: @Test(timeout = 1000): 限時操作,若超過制定時間,強制停止 @Test(expected = ArithmeticException.class): 預測拋出指定異常 2.3 mockito1.9.5創建mock對象不能對final,Anonymous ,primitive類進行mock。 when… thenRetrun; when… thenThrow doNothing().doRetrun(); doNothing.doThrow() anyInt、anyString、anyMap…..(參數匹配器) @Test public void argumentMatcherTest2(){ Map 2.4 robolectric3.1.2
1.測試跳轉 /** * Activity跳轉測試 */ @Test public void testStartActivity() { //按鈕點擊後跳轉到下一個Activity forwardBtn.performClick(); Intent expectedIntent = new Intent(sampleActivity, LoginActivity.class); Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity(); assertEquals(expectedIntent.getComponent(), actualIntent.getComponent()); } 2.模擬activity sampleActivity = Robolectric.setupActivity(SampleActivity.class);
/** * Toast的測試 */ @Test public void testToast() { //點擊按鈕,出現吐司 toastBtn.performClick(); assertEquals(ShadowToast.getTextOfLatestToast(), "we love UT"); } /** * Dialog的測試 */ @Test public void testDialog() { //點擊按鈕,出現對話框 dialogBtn.performClick(); AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog(); assertNotNull(latestAlertDialog); } /** * 測試控件狀態 */ @Test public void testViewState() { CheckBox checkBox = (CheckBox) sampleActivity.findViewById(R.id.checkbox); Button inverseBtn = (Button) sampleActivity.findViewById(R.id.btn_inverse); assertTrue(inverseBtn.isEnabled()); checkBox.setChecked(true); //點擊按鈕,CheckBox反選 inverseBtn.performClick(); assertTrue(!checkBox.isChecked()); inverseBtn.performClick(); assertTrue(checkBox.isChecked()); } /** * 資源文件訪問測試 */ @Test public void testResources() { Application application = RuntimeEnvironment.application; String appName = application.getString(R.string.app_name); String activityTitle = application.getString(R.string.title_activity_simple); assertEquals("LoveUT", appName); assertEquals("SimpleActivity", activityTitle); } /** * 測試廣播 */ @Test public void testBoradcast() { ShadowApplication shadowApplication = ShadowApplication.getInstance(); String action = "com.geniusmart.loveut.login"; Intent intent = new Intent(action); intent.putExtra("EXTRA_USERNAME", "geniusmart"); //測試是否註冊廣播接收者 assertTrue(shadowApplication.hasReceiverForIntent(intent)); //以下測試廣播接受者的處理邏輯是否正確 MyReceiver myReceiver = new MyReceiver(); myReceiver.onReceive(RuntimeEnvironment.application, intent); SharedPreferences preferences = RuntimeEnvironment.application.getSharedPreferences("account", Context.MODE_PRIVATE); assertEquals("geniusmart", preferences.getString("USERNAME", "")); } /** * 測試Fragment */ @Test public void testFragment() { SampleFragment sampleFragment = new SampleFragment(); //此api可以主動添加Fragment到Activity中,因此會觸發Fragment的onCreateView() SupportFragmentTestUtil.startFragment(sampleFragment); assertNotNull(sampleFragment.getView()); } 4.登錄場景測試 @Test public void loginSuccess() { emailView.setText("[email protected]"); passwordView.setText("123"); button.performClick(); ShadowApplication application = ShadowApplication.getInstance(); assertThat("Next activity has started", application.getNextStartedActivity(), is(notNullValue())); } @Test public void loginWithEmptyUsernameAndPassword() { button.performClick(); ShadowApplication application = ShadowApplication.getInstance(); assertThat("Next activity should not started", application.getNextStartedActivity(), is(nullValue())); assertThat("Show error for Email field ", emailView.getError(), is(notNullValue())); assertThat("Show error for Password field ", passwordView.getError(), is(notNullValue())); assertEquals(emailView.getError().toString(), RuntimeEnvironment.application.getString(R.string.error_field_required)); } @Test public void loginFailure() { emailView.setText("invalid@email"); passwordView.setText("invalidpassword"); button.performClick(); ShadowApplication application = ShadowApplication.getInstance(); assertThat("Next activity should not started", application.getNextStartedActivity(), is(nullValue())); assertThat("Show error for Email field ", emailView.getError(), is(notNullValue())); assertThat("Show error for Password field ", passwordView.getError(), is(notNullValue())); } 更多場景還需探索。。。 與espresso的對比Google 官方提供的一個易於測試 Android UI 的開源框架 , 於2013年10月推出它的 released 版本 , 目前最新版本已更新到2.x . 並且在AndroidStudio 2.2 預覽版中已經默認集成該測試庫 。 ViewMatchers - 在當前View層級去匹配指定的View . ViewActions - 執行Views的某些行為,如點擊事件 . ViewAssertions - 檢查Views的某些狀態,如是否顯示 . @RunWith(AndroidJUnit4.class) public class LoginUITest { @Rule public ActivityTestRule rule=new ActivityTestRule(LogingActivity.class,true); @Test public void login(){ //login onView(withId(R.id.userName)).perform(typeText("Jack"),closeSoftKeyboard()); onView(withId(R.id.password)).perform(typeText("1234"),closeSoftKeyboard()); onView(withText("登錄")).perform(click()); //verify onView(withId(R.id.content)).check(matches(isDisplayed())); } } espresso更偏向於自動化測試,集成後執行單元測試需要跑在Android手機上,其有個高級功能,根據你的點擊軌跡,自動生成自動測試代碼。 3. 覆蓋率
行覆蓋率:度量被測程序的每行代碼是否被執行,判斷標準行中是否至少有一個指令被執行。 類覆蓋率:度量計算class類文件是否被執行。 分支覆蓋率:度量if和switch語句的分支覆蓋情況,計算一個方法裏面的 總分支數,確定執行和不執行的 分支數量。 方法覆蓋率:度量被測程序的方法執行情況,是否執行取決於方法中是否有至少一個指令被執行。 指令覆蓋:計數單元是單個java二進制代碼指令,指令覆蓋率提供了代碼是否被執行的信息,度量完全 獨立源碼格式。 圈復雜度:在(線性)組合中,計算在一個方法裏面所有可能路徑的最小數目,缺失的復雜度同樣表示測試案例沒有完全覆蓋到這個模塊。
集成配置: apply plugin: ‘jacoco‘ android { buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile(‘proguard-android.txt‘), ‘proguard-rules.pro‘ } debug{ testCoverageEnabled true } } } jacoco { toolVersion = "0.7.5.201505241946" } jacoco覆蓋率報告分為兩種:
task jacocoTestReport(type:JacocoReport, dependsOn: "testDebugUnitTest") { println("=========jacocoTestReport start"); group = "Reporting" description = "Generate Jacoco coverage reports" classDirectories = fileTree( dir: "${project.buildDir}/intermediates/classes/debug", excludes: [‘**/R.class‘, ‘**/R$*.class‘, ‘**/*$ViewInjector*.*‘, ‘**/BuildConfig.*‘, ‘**/Manifest*.*‘] ) println("path==========>>" + "${project.buildDir}/intermediates/classes/debug") def coverageSourceDirs = "${project.projectDir}/src/main/java" println("coverageSourceDirs==========>>" + coverageSourceDirs) additionalSourceDirs = files(coverageSourceDirs) sourceDirectories = files(coverageSourceDirs) executionData = fileTree(dir: project.projectDir, includes:[‘**/*.exec‘, ‘**/*.ec‘]) reports { xml.enabled = true html.enabled = true } } task jacocoAndroidTestReport(type:JacocoReport,dependsOn:"connectedAndroidTest"){ group = "Reporting" description = "Generate Jacoco coverage reports after running tests." reports{ xml.enabled = true html.enabled = true csv.enabled = false } classDirectories = fileTree( dir : "$buildDir/intermediates/classes/debug", excludes : [ ‘**/*Test.class‘, ‘**/R.class‘, ‘**/R$*.class‘, ‘**/BuildConfig.*‘, ‘**/Manifest*.*‘ ] ) def coverageSourceDirs = [‘src/main/java‘] additionalSourceDirs = files(coverageSourceDirs) sourceDirectories = files(coverageSourceDirs) additionalClassDirs = files(coverageSourceDirs) executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec") } 結果展示總結單元測試的一些原則
|
[原]Android單測踩坑篇 |
在開始寫單測之前,已經調研了很久Android單測的框架以及demo,正好換了一個新的項目組,就從頭開始將單測框架應用到開發過程中,後續也方便可以使用TDD。 調研的框架:junit,mockito, roboletric,espresso,jacoco(覆蓋率報告) 具體場景:網絡請求,todomvp的單測方式,UI測試等。 理想永遠是美好的,擼起袖子開始幹的時候,就會發現還有很多崎嶇需要去踏平。 首先,我放棄了espresso(雖然是google官方出的測試框架),主要原因是espresso依賴真機或模擬器。我相信大部分應用應該都有模擬器判斷,在模擬器環境下禁止啟動app,為了寫單測,還得改代碼,很難受;另外多數開發團隊應該都是持續集成,跑espresso,還需要有一臺手機常年插在構建機上,很麻煩。這並不是說espresso毫無用處,espresso對於Android框架支持非常完善,基本上所有UI的測試都可以實現。另外,espresso還有一個非常強大的功能,適合測試人員手工測試:錄制測試過程,自動生成測試代碼。比方說:你進入app後,按照正確測試流程點擊一遍後,保存下來,下一次開發人員有了些許改動,你只需要運行一次之前自動生成的測試代碼,應用便會按照之前的流程進行測試,如出現異常,則測試不通過!非常強大(據團隊的IOS工程師說,ios也有一樣的測試框架,個人覺得測試人員可以考慮一下這個)。 所以最初我采用的Android單測框架就是:junit + mockito + roboletric + jacoco。 junit和mockito就不要多講了,都是java單元測試最基本的框架。在說roboletric之前,得先說一下powerMock,mockito無法mock static方法,而powermock則解決了這個問題。所以powermock和mockito配合使用,基本就可以覆蓋絕大部分情況。不過由於powermock和mockito是兩個團隊實現的,經常出現版本不兼容的情況,建議直接使用powermock內部引用的mockito,這樣就不會沖突了。 貼上地址:
在使用框架時,註意對應版本。頁面中的第一句話:
我使用的完整配置如下:
原本使用powermock的版本是1.7.X,發現使用的過程中各種報錯,還是使用了官方的1.6.6版本,不知道這兩個團隊什麽時候兼容能做的很完善。 附上使用BaseTest:
@Config(constants = BuildConfig.class , sdk = 21):原本想使用23的sdk版本,會有不兼容問題。 @PowerMockIgnore({“org.mockito.“, “org.robolectric.“, “android.“, “org.json.“, “sun.security.“, “javax.net.“}):根據我的理解:powermock類加載器忽略以上類的加載。 @PrepareForTest({CPSNetUtils.class, Common.class}):想mock static的方法,必須加上此註解。 基本配置如上,若上述配置都沒問題了,就可以真正開始寫單測了。目前Android端的框架使用的是google推薦的mvp框架,優缺點,網上有很多文章,就不在贅述。github地址如下:
不可否認的一點,使用mvp框架後單測實現會簡單很多,極大程度減少了對view的測試。貼上一段業務測試代碼:
細心的讀者會發現在feedbackSuccessTest測試方法中,我開了個線程睡了半秒鐘,這時候就要說到網絡請求的單元測試了。網絡交互是客戶端最頻繁的場景,而網絡的不穩定,會導致客戶端出現很多難以預知的情況,崩潰,閃退都有可能發生。所以對於網絡請求的單測是重中之重。我使用的網絡庫是okhttp,而okhttp有一個很強大的功能:Interceptor。interceptor可以攔截網絡請求,可以處理完後繼續發送,也可以直接直接返回。廢話不多說,上代碼:
當然也可以模擬異常返回,404什麽的都可以。另外okhttp使用的是建造者模式,客戶端網絡請求OkHttpClient都是一致的,故可以使用類似代碼直接mock返回:
單測寫完,總得看到點數據報告吧。這時候就需要覆蓋率報告了。Android studio自帶了jacoco 插件生成覆蓋率報告。 行覆蓋率:度量被測程序的每行代碼是否被執行,判斷標準行中是否至少有一個指令被執行。 類覆蓋率:度量計算class類文件是否被執行。 分支覆蓋率:度量if和switch語句的分支覆蓋情況,計算一個方法裏面的總分支數,確定執行和不執行的分支數量。 方法覆蓋率:度量被測程序的方法執行情況,是否執行取決於方法中是否有至少一個指令被執行。 指令覆蓋:計數單元是單個java二進制代碼指令,指令覆蓋率提供了代碼是否被執行的信息,度量完全 獨立源碼格式。 圈復雜度:在(線性)組合中,計算在一個方法裏面所有可能路徑的最小數目,缺失的復雜度同樣表示測試案例沒有完全覆蓋到這個模塊。
jacoco的覆蓋率報告分為兩種:
方法也不盡相同。生成java層代碼的任務在前面代碼中已經貼出。 生成運行app期間執行的覆蓋率報告代碼如下:
以上代碼並未測試過,點擊執行測試後,會啟動app,並在會生成.ec文件,通過解析.ec文件, 可以生成覆蓋率報告。因為前面放棄esspreso時就說過放棄真機測試,所以此方法也未使用。
另外,中間遇到一個大坑,被坑了很久。就是jacoco和roboletric也有版本不兼容的問題。。。使用最新的jacoco的版本,發現生成的覆蓋率報告始終為0 在stackoverflow中找了查了很久,最後在一個帖子裏看到,在jacoco 0.7.3以上的版本使用roboletric就始終為0,嘗試了多個版本,發現 0.7.1.201405082137 是OK的。千萬不要隨便升級版本,否則會出現異想不到的問題。。 好了,下面就看一下覆蓋率報告: 後續會接入jenkins,等踩完坑,再補一篇文章。 作者:u012933335 發表於 2018/02/07 18:32:37 原文鏈接 https://blog.csdn.net/u012933335/article/details/79283201 閱讀:86 |
單元測試匯總