【測試】- Android單元測試初探

簡介

最近重新開始學習了一下,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
    @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里,如果啟動繼承于AppCompatActivity的Activity會報錯,目前沒有找到解決辦法,可以正常啟動繼承于Activity的Activity。
  • 網絡請求測試
    比如在業務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
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。