簡介
最近重新開始學習了一下,Android的單元測試,以前都是馬馬虎虎看了看,覺得用處不大,還要寫代碼,麻煩。最近動手去寫了一些單元測試,在有些情況下,相比通過安裝App,界面操作來測試要方便,快捷很多。特別是項目復雜的時候。
其實很多開發者都知道單元測量,也能寫一些簡單的單元測試,但是就我工作以來,很少,基本沒有看到項目中有編寫單元測試的。因為編寫額外的代碼,麻煩,加上不熟悉,就更加不想寫了。我以前也是這種想法,但是最近的接觸,然后覺得,做單元測試還是很有必要的。
舉例
-
網絡請求
比如測試一個功能,而這個功能會進行網絡請求,當出現問題時,我們得拿到網絡請求返回的數據,這樣才知道是后端問題,還是前端邏輯問題。而進入這個功能需要進行好幾步操作,如果需要更改什么配置,還需要重新安裝apk,想想過程都復雜,而且重新安裝apk可以一個耗時的過程。
這時候,我們就可以用單元測試,可以在不重新安裝apk,不用去幾番操作就可以拿到網絡請求的結果。下面會實戰舉例。
api測試
這種情況,用單元測試最快捷的,只需要在“test”目錄下編寫代碼,在本機運行即可,比起安裝apk,然后去點點點方便很多。
缺點
缺點當然就是要編寫額外的測試代碼,如果業務邏輯有改動,測試代碼也得相應改動,存在后期維護,還有一點點的學習成本。不過總得來說,還是利大于弊的。
參考
studio_test
training_testing
android-testing-templates
單元測試
單元測試可以直接在業務代碼的module下編寫代碼,也可以專門建一個單元測試module。
業務module下做單元測試
我們在新建module的時候,Android Studio會在資源目錄src下生成“androidTest”和“test”兩個目錄,并且有生成一個簡單的單元測試文件。單元測試需要的相應依賴也會配置好。你只需要在文件中編寫測試代碼即可。
androidTest目錄:
這些測試在硬件設備或模擬器上運行。這些測試有權使用 Instrumentation API,可讓您獲取某些信息(例如您要測試的應用的 Context,并且可讓您通過測試代碼來控制受測應用。在編寫集成和功能界面測試來自動執行用戶交互時,或者當您的測試具有模擬對象無法滿足的 Android 依賴項時,可以使用這些測試。test目錄:
這些測試在計算機的本地 Java 虛擬機 (JVM) 上運行。如果您的測試沒有 Android 框架依賴項,或者您可以模擬 Android 框架依賴項,使用這些測試可以最大限度地縮短執行時間。
單獨的測試module
我們可以像創建lib庫那樣,給需要測試的工程創建一個用于單元測試的module。
- 第一步
創建一個Android Library module - 第二步
將“apply plugin: 'com.android.library'”改成“apply plugin: 'com.android.test'” - 第三步
添加測試所用的依賴庫時,和其它module一樣用“implementation”等,而不是“androidTestImplementation”等 - 第四步
指定需要被測試的module,在“AndroidManifest.xml”下:<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.pds.testapp" > <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.pds.blog"/> </manifest>
在build.gradle中指定需要被測試的module: targetProjectPath ':app'。
android:targetPackage 指定需要被測試module的包名,targetProjectPath這是module在工程中的路徑。
測試相關配置
具體字段的意思,可以參考官網。
android {
compileSdkVersion app.compileSdkVersion
defaultConfig {
minSdkVersion 26
targetSdkVersion app.targetSdkVersion
testApplicationId "com.pds.test.${project.name}"
testHandleProfiling true
testFunctionalTest true
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
// 配置需要被測試的工程,和settings.gradle名字一致
targetProjectPath ':app'
javaCompileOptions {annotationProcessorOptions {includeCompileClasspath = true}}}
testOptions {
reportDir "$rootDir/test_app/test-reports"
resultsDir "$rootDir/test_app/test-results"
// 要僅為本地單元測試指定選項,請配置 testOptions {} 中的 unitTests {} 代碼塊。
unitTests {
// 如果您的測試依賴于資源 默認情況下,Android Studio 3.4 及更高版本提供編譯版本的資源。
includeAndroidResources = true
all {
jvmArgs '-XX:MaxPermSize=256m'
if (it.name == 'test_app') {systemProperty 'debug', 'true'}
}
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
基礎依賴
dependencies {
implementation 'androidx.test:rules:1.2.0'
implementation 'androidx.test:runner:1.2.0'
implementation 'org.hamcrest:hamcrest-core:1.3'
implementation 'androidx.test.ext:junit:1.1.1'
implementation 'androidx.test.ext:truth:1.2.0'
implementation 'com.google.truth:truth:0.42'
}
實戰
- 啟動Activity
在測試module里,如果啟動繼承于AppCompatActivity的Activity會報錯,目前沒有找到解決辦法,可以正常啟動繼承于Activity的Activity。@Test public void launchMarqueeTextPage() { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); Intent intent = new Intent(context, GlideTestActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ActivityTestRule<GlideTestActivity> activityTestRule = new ActivityTestRule<>(GlideTestActivity.class, true, false); activityTestRule.launchActivity(intent); // 讓界面不自動退出 try { CountDownLatch countdown = new CountDownLatch(1); countdown.await(); } catch (InterruptedException e) { e.printStackTrace(); } } }
- 網絡請求測試
比如在業務module,定義了一套網絡請求的api,那么我們可以直接引用業務module里面寫好的網絡api,來發起網絡請求。@Test public void getPhoneNumber() { Disposable observable = ApiManager .getUserApi() .getPhoneNumber("") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .compose(SchedulersCompat.<BaseEntity<DataEntity>>applyIoSchedulers()) .map(new HttpResultFunc<DataEntity>()) .subscribe(new Consumer<DataEntity>() { @Override public void accept(DataEntity data) throws Exception { } }, new ErrorConsumer() { @Override public void accept(Throwable throwable) { super.accept(throwable); } }); }
- 自定義Rule
@Rule public TipsRule tipsRule = new TipsRule();
public class TipsRule implements TestRule { @Override public Statement apply(final Statement base, final Description description) { return new Statement() { // evaluate前執行方法相當于@Before @Override public void evaluate() throws Throwable { // 獲取測試方法的名字 String methodName = description.getMethodName(); System.out.println("-------"+ methodName + "------>測試開始!"); // 運行的測試方法 base.evaluate(); // evaluate后執行方法相當于@After System.out.println("-------"+ methodName + "------>測試結束!"); } }; } }
- 自定義Matcher
@Test public void testAssertThatMatcher(){assertThat("19508460000",new MobilePhoneMatcher());}
public class MobilePhoneMatcher extends BaseMatcher<String> { /** * 進行斷言判定,返回true則斷言成功,否則斷言失敗 */ @Override public boolean matches(Object item) { if (null == item) return false; Pattern pattern = Pattern.compile("(1|861)(3|5|7|8)\\d{9}$*"); Matcher matcher = pattern.matcher((String) item); return matcher.find(); } /** * 給期待斷言成功的對象增加描述 */ @Override public void describeTo(Description description) { description.appendText("預計此字符串是手機號碼!"); } /** * 給斷言失敗的對象增加描述 */ @Override public void describeMismatch(Object item, Description description) { description.appendText(item.toString() + "不是手機號碼!"); } }
monkeyrunner
monkeyrunner 工具提供了一個 API,用于編寫可從 Android 代碼外部控制 Android 設備或模擬器的程序。使用 monkeyrunner,您可以編寫一個 Python 程序,以便安裝 Android 應用或測試軟件包,運行它,向其發送按鍵,截取其界面的屏幕截圖,并將屏幕截圖存儲到工作站中。monkeyrunner 工具主要用于在功能/框架級測試應用和設備以及運行單元測試套件,但您也可以自由地將其用于其他目的。參考:monkeyrunner
用python編寫測試腳本,然后用monkeyrunner工具運行。
monkeyrunner可執行文件存在于sdk/tools/bin目錄下,編寫好的python腳本用monkeyrunner命令執行,例如: monkeyrunner monkey.py。單獨執行python文件是不行的,沒法導入python中用到的Java庫。
擴展
之所以可以在python里面寫Java,需要Jython庫的支持,我們可以在工程中導入該庫,那么我們就可以用Java代碼執行python腳本。python腳本里面編寫Java代碼。
-
python腳本
截屏2020-05-22 下午2.16.09.png -
Java代碼
截屏2020-05-22 下午2.17.01.png