==》完整項目單元測試學習案例html
衆所周知,一個好的項目須要不斷地打造,而一些有效的測試則是加速這一過程的利器。本篇博文將帶你瞭解並逐步深刻Android單元測試。java
單元測試就是針對類中的某一個方法進行驗證是否正確的過程,單元就是指獨立的粒子,在Android和Java中大都是指方法。android
使用單元測試能夠提升開發效率,當項目隨着迭代愈來愈大時,每一次編譯、運行、打包、調試須要耗費的時間會隨之上升,所以,使用單元測試能夠不需這一步驟就能夠對單個方法進行功能或邏輯測試。 同時,爲了能測試每個細分功能模塊,須要將其相關代碼抽成相應的方法封裝起來,這也在必定程度上改善了代碼的設計。由於是單個方法的測試,因此能更快地定位到bug。git
單元測試case須要對這段業務邏輯進行驗證。在驗證的過程當中,開發人員能夠深度瞭解業務流程,同時新人來了看一下項目單元測試就知道哪一個邏輯跑了多少函數,須要注意哪些邊界——是的,單元測試作的好和文檔同樣具有業務指導能力。github
Android測試主要分爲三個方面:編程
單元測試(Junit四、Mockito、PowerMockito、Robolectric)
UI測試(Espresso、UI Automator)
壓力測試(Monkey)
複製代碼
Junit4是事實上的Java標準測試庫,而且它是JUnit框架有史以來的最大改進,其主要目標即是利用Java5的Annotation特性簡化測試用例的編寫。json
dependencies {
...
testImplementation 'junit:junit:4.12'
}
複製代碼
@Test 指明這是一個測試方法 (@Test註解能夠接受2個參數,一個是預期錯誤
expected,一個是超時時間timeout,
格式如 @Test(expected = IndexOutOfBoundsException.class),
@Test(timeout = 1000)
@Before 在全部測試方法以前執行
@After 在全部測試方法以後執行
@BeforeClass 在該類的全部測試方法和@Before方法以前執
行 (修飾的方法必須是靜態的)@AfterClass 在該類的全部測試方法和@After
方法以後執行(修飾的方法必須是靜態的)
@Ignore 忽略此單元測試
複製代碼
此外,不少時候,由於某些緣由(好比正式代碼尚未實現等),咱們可能想讓JUnit忽略某些方法,讓它在跑全部測試方法的時候不要跑這個測試方法。要達到這個目的也很簡單,只須要在要被忽略的測試方法前面加上@Ignore就能夠了api
assertEquals(expected, actual) 判斷2個值是否相等,相等則測試經過。
assertEquals(expected, actual, tolerance) tolerance 誤差值
複製代碼
注意:上面的每個方法,都有一個重載的方法,能夠加一個String類型的參數,表示若是驗證失敗的話,將用這個字符串做爲失敗的結果報告。瀏覽器
public class JsonChaoRule implements TestRule {
@Override
public Statement apply(final Statement base, final Description description) {
Statement repeatStatement = new Statement() {
@Override
public void evaluate() throws Throwable {
//測試前的初始化工做
//執行測試方法
base.evaluate();
//測試後的釋放資源等工做
}
};
return repeatStatement;
}
}
複製代碼
而後在想要的測試類中使用@Rule註解聲明使用JsonChaoRule便可(注意被@Rule註解的變量必須是final的):微信
@Rule
public final JsonChaoRule repeatRule = new JsonChaoRule();
複製代碼
1.編寫測試類。
2.鼠標右鍵點擊測試類,選擇選擇Go To->Test
(或者使用快捷鍵Ctrl+Shift+T,此快捷鍵可
以在方法和測試方法之間來回切換)在Test/java/項目
測試文件夾/下自動生成測試模板。
3.使用斷言(assertEqual、assertEqualArrayEquals等等)進行單元測試。
4.右鍵點擊測試類,Run編寫好的測試類。
複製代碼
點擊Android Studio中的Gradle projects下的app/Tasks/verification/test便可同時測試module下全部的測試類(案例),並在module下的build/reports/tests/下生成對應的index.html測試報告。
優勢:速度快,支持代碼覆蓋率等代碼質量的檢測工具,
缺點:沒法單獨對Android UI,一些類進行操做,與原生JAVA有一些差別。
複製代碼
可能涉及到的額外的概念:
打樁方法:使方法簡單快速地返回一個有效的結果。
測試驅動開發:編寫測試,實現功能使測試經過,而後不斷地使用這種方式實現功能的快速迭代開發。
Mockito 是美味的 Java 單元測試 Mock 框架,mock能夠模擬各類各樣的對象,從而代替真正的對象作出但願的響應。
testImplementation 'org.mockito:mockito-core:2.7.1'
複製代碼
Person mPerson = mock(Person.class);
複製代碼
在JUnit框架下,case(即每個測試點,帶@Test註解的那個函數)也是個函數,直接調用這個函數就不是case,和case是無關的,二者並不會相互影響,能夠直接調用以減小重複代碼。單元測試不該該對某一個條件過分耦合,所以,須要用mock解除耦合,直接mock出網絡請求獲得的數據,單獨驗證頁面對數據的響應。
when(iMathUtils.sum(1, 1)).thenReturn(2);
doReturn(3).when(iMathUtils).sum(1,1);
//給方法設置樁能夠設置屢次,只會返回最後一次設置的值
doReturn(2).when(iMathUtils).sum(1,1);
//驗證方法調用次數
//方法調用1次
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
//方法調用3次
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")
, Mockito.times(3).thenReturn(true);
//verify方法用於驗證「模仿對象」的互動或驗證發生的某些行爲
verify(mPerson, atLeast(2)).getAge();
//參數匹配器,用於匹配特定的參數
any()
contains()
argThat()
when(mPerson.eat(any(String.class))).thenReturn("米飯");
//除了mock()外,spy()也能夠模擬對象,spy與mock的
//惟一區別就是默認行爲不同:spy對象的方法默認調用
//真實的邏輯,mock對象的方法默認什麼都不作,或直接
//返回默認值
//若是要保留原來對象的功能,而僅僅修改一個或幾個
//方法的返回值,能夠採用spy方法,無參構造的類初始
//化也使用spy方法
Person mPerson = spy(Person.class);
//檢查入參的mocks是否有任何未經驗證的交互
verifyNoMoreInteractions(iMathUtils);
複製代碼
簡單的測試會使總體的代碼更簡單,更可讀、更可維護。若是你不能把測試寫的很簡單,那麼請在測試時重構你的代碼。
優勢:豐富強大的方式驗證「模仿對象」的互動或驗證發生的某些行爲
缺點:Mockito框架不支持mock匿名類、final類、static方法、private方法。
複製代碼
雖然,static方法可使用wrapper靜態類的方式實現mockito的單元測試,可是,畢竟過於繁瑣,所以,PowerMockito由此而來。
PowerMockito是一個擴展了Mockito的具備更強大功能的單元測試框架,它支持mock匿名類、final類、static方法、private方法
testImplementation 'org.powermock:powermock-module-junit4:1.6.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.5'
複製代碼
//使用PowerMock須加註解@PrepareForTest和@RunWith(PowerMockRunner.class)(@PrepareForTest()裏寫的
// 是對應方法所在的類 ,mockito支持的方法使用PowerMock的形式實現時,能夠不加這兩個註解)
@PrepareForTest(T.class)
@RunWith(PowerMockRunner.class)
//mock含靜態方法或字段的類
PowerMockito.mockStatic(Banana.class);
//Powermock提供了一個Whitebox的class,能夠方便的繞開權限限制,能夠get/set private屬性,實現注入。
//也能夠調用private方法。也能夠處理static的屬性/方法,根據不一樣需求選擇不一樣參數的方法便可。
修改類裏面靜態字段的值
Whitebox.setInternalState(Banana.class, "COLOR", "藍色");
//調用類中的真實方法
PowerMockito.when(banana.getBananaInfo()).thenCallRealMethod();
//驗證私有方法是否被調用
PowerMockito.verifyPrivate(banana, times(1)).invoke("flavor");
//忽略調用私有方法
PowerMockito.suppress(PowerMockito.method(Banana.class, "flavor"));
//修改私有變量
MemberModifier.field(Banana.class, "fruit").set(banana, "西瓜");
//使用PowerMockito mock出來的對象能夠直接調用final方法
Banana banana = PowerMockito.mock(Banana.class);
//whenNew 方法的意思是以後 new 這個對象時,返回某個被 Mock 的對象而不是讓真的 new
//新的對象。若是構造方法有參數,能夠在withNoArguments方法中傳入。
PowerMockito.whenNew(Banana.class).withNoArguments().thenReturn(banana);
複製代碼
testImplementation "org.powermock:powermock-module-junit4-rule:1.7.4"
testImplementation "org.powermock:powermock-classloading-xstream:1.7.4"
複製代碼
使用示例以下:
@Rule
public PowerMockRule mPowerMockRule = new PowerMockRule();
複製代碼
經過註解@Parameterized.parameters提供一系列數據給構造器中的構造參數或給被註解@Parameterized.parameter註解的public全局變量
RunWith(Parameterized.class)
public class ParameterizedTest {
private int num;
private boolean truth;
public ParameterizedTest(int num, boolean truth) {
this.num = num;
this.truth = truth;
}
//被此註解註解的方法將把返回的列表數據中的元素對應注入到測試類
//的構造函數ParameterizedTest(int num, boolean truth)中
@Parameterized.Parameters
public static Collection providerTruth() {
return Arrays.asList(new Object[][]{
{0, true},
{1, false},
{2, true},
{3, false},
{4, true},
{5, false}
});
}
// //也可不使用構造函數注入的方式,使用註解注入public變量的方式
// @Parameterized.Parameter
// public int num;
// //value = 1指定括號裏的第二個Boolean值
// @Parameterized.Parameter(value = 1)
// public boolean truth;
@Test
public void printTest() {
Assert.assertEquals(truth, print(num));
System.out.println(num);
}
private boolean print(int num) {
return num % 2 == 0;
}
}
複製代碼
Robolectric經過一套能運行在JVM上的Android代碼,解決了在Java單元測試中很難進行Android單元測試的痛點。
//Robolectric核心
testImplementation "org.robolectric:robolectric:3.8"
//支持support-v4
testImplementation 'org.robolectric:shadows-support-v4:3.4-rc2'
//支持Multidex功能
testImplementation "org.robolectric:shadows-multidex:3.+"
複製代碼
首先給指定的測試類上面進行配置
@RunWith(RobolectricTestRunner.class)
//目前Robolectric最高支持sdk版本爲23。
@Config(constants = BuildConfig.class, sdk = 23)
複製代碼
下面是一些經常使用用法
//當Robolectric.setupActivity()方法返回的時候,
//默認會調用Activity的onCreate()、onStart()、onResume()
mTestActivity = Robolectric.setupActivity(TestActivity.class);
//獲取TestActivity對應的影子類,從而能獲取其相應的動做或行爲
ShadowActivity shadowActivity = Shadows.shadowOf(mTestActivity);
Intent intent = shadowActivity.getNextStartedActivity();
//使用ShadowToast類獲取展現toast時相應的動做或行爲
Toast latestToast = ShadowToast.getLatestToast();
Assert.assertNull(latestToast);
//直接經過ShadowToast簡單工廠類獲取Toast中的文本
Assert.assertEquals("hahaha", ShadowToast.getTextOfLatestToast());
//使用ShadowAlertDialog類獲取展現AlertDialog時相應的
//動做或行爲(暫時只支持app包下的,不支持v7。。。)
latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
Assert.assertNull(latestAlertDialog);
//使用RuntimeEnvironment.application能夠獲取到
//Application,方便咱們使用。好比訪問資源文件。
Application application = RuntimeEnvironment.application;
String appName = application.getString(R.string.app_name);
Assert.assertEquals("WanAndroid", appName);
//也能夠直接經過ShadowApplication獲取application
ShadowApplication application = ShadowApplication.getInstance();
Assert.assertNotNull(application.hasReceiverForIntent(intent));
複製代碼
自定義Shadow類
@Implements(Person.class)
public class ShadowPerson {
@Implementation
public String getName() {
return "AndroidUT";
}
}
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class,
sdk = 23,
shadows = {ShadowPerson.class})
Person person = new Person();
//實際上調用的是ShadowPerson的方法,輸出JsonChao
Log.d("test", person.getName());
ShadowPerson shadowPerson = Shadow.extract(person);
//測試經過
Assert.assertEquals("JsonChao", shadowPerson.getName());
}
複製代碼
注意: 異步測試出現一些問題(好比改變一些編碼習慣,好比回調函數不能寫成匿名內部類對象,須要定義一個全局變量,並破壞其封裝性,即提供一個get方法,供UT調用),解決方案使用Mockito來結合進行測試,將異步轉爲同步。
優勢:支持大部分Android平臺依賴類底層的引用與模擬。
缺點:異步測試有些問題,須要結合一些框架來配合完成更多功能。
複製代碼
Jacoco的全稱爲Java Code Coverage(Java代碼覆蓋率),能夠生成java的單元測試代碼覆蓋率報告。
在應用Module下加入jacoco.gradle自定義腳本,app.gradle apply from它,同步,便可看到在app的Task下生成了Report目錄,Report目錄 下生成了JacocoTestReport任務。
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.7.7.201606060606" //指定jacoco的版本
reportsDir = file("$buildDir/JacocoReport") //指定jacoco生成報告的文件夾
}
//依賴於testDebugUnitTest任務
task jacocoTestReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {
group = "reporting" //指定task的分組
reports {
xml.enabled = true //開啓xml報告
html.enabled = true //開啓html報告
}
def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug",
includes: ["**/*Presenter.*"],
excludes: ["*.*"])//指定類文件夾、包含類的規則及排除類的規則,
//這裏咱們生成全部Presenter類的測試報告
def mainSrc = "${project.projectDir}/src/main/java" //指定源碼目錄
sourceDirectories = files([mainSrc])
classDirectories = files([debugTree])
executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec")//指定報告數據的路徑
}
複製代碼
在Gradle構建板塊Gradle.projects下的app/Task/verification下,其中testDebugUnitTest構建任務會生成單元測試結果報告,包含xml及html格式,分別對應test-results和reports文件夾;jacocoTestReport任務會生成單元測試覆蓋率報告,結果存放在jacoco和JacocoReport文件夾。
生成的JacocoReport文件夾下的index.html即對應的單元測試覆蓋率報告,用瀏覽器打開後,能夠看到覆蓋狀況被不一樣的顏色標識出來,其中綠色表示代碼被單元測試覆蓋到,黃色表示部分覆蓋,紅色則表示徹底沒有覆蓋到。
要驗證程序正確性,必然要給出全部可能的條件(極限編程),並驗證其行爲或結果,纔算是100%覆蓋條件。實際項目中,驗證通常條件和邊界條件就OK了。
在實際項目中,單元測試對象與頁面是一對一的,並不建議跨頁面,這樣的單元測試耦合太大,維護困難。 須要寫完後,看覆蓋率,找出單元測試中沒有覆蓋到的函數分支條件等,而後繼續補充單元測試case列表,並在單元測試工程代碼中補上case。 直到規劃的頁面中全部邏輯的重要分支、邊界條件都被覆蓋,該項目的單元測試結束。
能夠從公司項目小規模使用,造成本身的單元測試風格後,就能夠跟大範圍地推廣了。
一、必知必會 | Android 測試相關的方方面面都在這兒
若是這個庫對您有很大幫助,您願意支持這個項目的進一步開發和這個項目的持續維護。你能夠掃描下面的二維碼,讓我喝一杯咖啡或啤酒。很是感謝您的捐贈。謝謝!
歡迎關注個人微信:
bcce5360
微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~