• <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>
  • Android 單元測試實踐

    發表于:2016-06-29來源:推酷作者:HelloCsl點擊數: 標簽:Android
    在計算機編程中,單元測試(Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。但是什么叫"程序單元

    什么是單元測試

    在計算機編程中,單元測試(Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。但是什么叫"程序單元"呢?是一個模塊、還是一個類、還是一個方法(函數)呢?不同的人、不同的語言,都有不同的理解。一般的定義,尤其是是在OOP領域,是一個類的一個方法。在此,我們也這樣理解:單元測試,是為了測試某一個類的某一個方法能否正常工作,而寫的測試代碼。

    單元測試的三個步驟:

    • setup:即新建出待測試的類、設置一些前提條件等
    • 執行動作:即調用被測類的被測方法,并獲取返回結果
    • 驗證結果:驗證獲取的結果跟預期的結果是一樣的

    單元測試不是集成測試

    這里需要強調一個觀念,那就是單元測試只是測試一個方法單元,它不是測試一整個流程。舉個例子來說,一個Login頁面,上面有兩個輸入框和一個button。兩個輸入框分別用于輸入用戶名和密碼。點擊button以后,有一個UserManager會去執行performlogin操作,然后將結果返回,更新頁面。那么我們給這個東西做單元測試的時候,不是測這一整個login流程。這種整個流程的測試:給兩個輸入框設置正確的用戶名和密碼,點擊login button,最后頁面得到更新。叫做集成測試,而不是單元測試。當然,集成測試也是有他的必要性的,然而這不是每個程序員應該花多少精力所在的地方。為什么是這樣呢?因為集成測試設置起來很麻煩,運行起來很慢,在保證代碼質量、改善代碼設計方面更起不到任何作用,因此它的重要程度并不是那么高

    Android中的單元測試

    Android中的單元測試分為兩種,Local Unit Tests 和 Instrumented Tests,前者運行在JVM,后者需要運行再Android設備

    Local Unit Tests

    Local Unit Tests運行在本地JVM,不需要安裝APP,所以運行時間很快。也因此不能依賴Android的API,所以大多數時候需要用Mock的形式來做替換(后面會提到)


    LocalUnitTests

    配置

    • 測試代碼目錄:module-name/src/test/java
    • 一般使用到的測試框架

      • JUnit4
      • Mockito

    使用Gradle添加相應的庫

      dependencies {
        // Required -- JUnit 4 framework
        testCompile 'junit:junit:4.12'
        // Optional -- Mockito framework
        testCompile 'org.mockito:mockito-core:1.10.19'
    }

    使用

    JUnit4

    這是Java界用的最廣泛,也是最基礎的一個框架,其他的很多框架,包括我們后面會看到的Mockito,都是基于或兼容JUnit4的。 使用比較簡單,最多的是其Assert類提供的assertXXX方法。

    假設這樣的一個類需要測試

    public class Calculator {
    
        public int add(int one, int another) {
            return one + another;
        }
    
        public int multiply(int one, int another) {
            return one * another;
        }

    如果不使用單元測試框架,我們可能需要這樣來驗證這段代碼的正確性:

    public class CalculatorTest {
        public static void main(String[] args) {
            Calculator calculator = new Calculator();
            int sum = calculator.add(1, 2);
            if(sum == 3) {
                System.out.println("add() works!")
            } else {
                System.out.println("add() does not works!")
            }
    
            int product = calculator.multiply(2, 4);
            if (product == 8) {
                System.out.println("multiply() works!")
            } else {
                System.out.println("multiply() does not works!")
            }
        }
    }

    然后我們再通過某種方式,比如命令行或IDE,運行這個CalculatorTestmain方法,在看著terminal的輸出,才知道測試是通過還是失敗。想一下,如果我們有很多的類,每個類都有很多方法,那么就要寫一堆這樣的代碼,每個類對于一個含有main方法的test類,同時main方法里面會有一堆代碼。這樣既寫起來痛苦,跑起來更痛苦,比如說,你怎么樣一次性跑所有的測試類呢?所以,一個測試框架為我們做的最基本的事情,就是允許我們按照某種更簡單的方式寫測試代碼,把每一個測試單元寫在一個測試方法里面,然后它會自動找出所有的測試方法,并且根據你的需要,運行所有的測試方法,或者是運行單個測試方法,或者是運行部分測試方法等等。 對于上面的例子,如果使用Junit的話,我們可以按照如下的方式寫測試代碼:

    public class CalculatorTest {
        Calculator mCalculator;
    
        @Before
        public void setup() {
            mCalculator = new Calculator();
        }
    
        @Test
        public void testAdd() throws Exception {
            int sum = calculator.add(1, 2);
            Assert.assertEquals(3, sum);
        }
    
        @Test
        public void testMultiply() throws Exception {
            int product = calculator.multiply(2, 4);
            Assert.assertEquals(8, product);
        }
    
    }

    上面的@Before修飾的方法,會在測試開始前調用,這里是新建了一個Calculator對象,所以之后的一些測試單元都可以直接使用這個實例,@Test修飾的方法,是用來需要測試的單元,例如testAdd方法是用來測試Calculator類的加法操作,測試單元內使用Assert類提供的assertXXX方法來驗證結果。如果還有其他的測試方法,則以此類推。 另外還有一些可能需要用到的修飾符,如@After,@BeforeClass,@AfterClass等。

    Mockito

    Mockito的兩個重要的功能是,驗證Mock對象的方法的調用和可以指定mock對象的某些方法的行為。(對于不懂Mock概念的同學來說,第一次看到的確很可能很難理解)

    為什么要使用Mockito?

    這是項目中的一個例子:

    /**
     * @param <T> 用于過濾的實體類型
     */
    public interface BaseItemFilter<T> {
        /**
         * @param item
         * @return true:不過濾;false:需要過濾
         */
        boolean accept(T item);
    
    }

    BaseItemFilter是用來判斷某種指定類型的實體是否需要過濾的,類似java中的FileFilter,目的是為了用了過濾不符合要求的實體。

    以下是我們的關鍵服務過濾器的實現:

    public class EssentialProcessFilter implements BaseItemFilter<RunningAppBean> {
    
        /**
         * 系統關鍵進程及用戶主要的進程
         */
        private static HashSet<String> sCoreList = new HashSet<String>();
    
        /**
         * 加載系統核心進程列表
         * @param context
         */
        public static void loadCoreList(Context context) {
            if (sCoreList.isEmpty()) {
                final Resources r = context.getResources();
                String[] corePackages = r.getStringArray(R.array.default_core_list);
                Collections.addAll(sCoreList, corePackages);
            }
        }
    
        @Override
        public boolean accept(RunningAppBean appModle) {
            return appModle != null && !(isEssentialProcess(appModle.mPackageName) || isEssentialProcessMock(appModle.mPackageName, appModle.mIsSysApp));
        }
    
        /**
         * 判斷進程是否屬于重要進程
         * @param process
         * @return
         */
        public static boolean isEssentialProcess(String process) {
            return sCoreList.contains(process);
        }
    
        /**
         * 系統關鍵進程關鍵詞模糊匹配
         * @param packageName
         * @param isSystemApp
         * @return
         */
        public static boolean isEssentialProcessMock(String packageName, boolean isSystemApp) {
            return 省略...額外的一些判斷;
        }
    
    }

    可以看到,這里的關鍵服務的判斷的判斷規則可以分兩部分,一個是從String.xml中預設的一段Arrays數組查找是否右符合的,這個需要在初始化或某個時機預先調用EssentialProcessFilter#loadCoreList(Context context)方法來加載,另外的一個判斷是在EssentialProcessFilter#isEssentialProcessMock方法中定義,這個類中accept方法,定義了只要符合其中一種規則,那么我們就需要把它過濾。

    這個時候我們來寫單元測試,你一開始就會發現你沒有辦法新建一個Context對象來讀取String.xml,即使你想盡任何方法新建一個ContextImpl實例,最后你還是會出錯的,主要原因再在于Gradle運行Local Unit Test 所使用的android.jar里面所有API都是空實現,并拋出異常的。 現在想想,我們實際上并不需要真的讀取String.xml,我們需要驗證的是記錄在我們的關鍵列表集合是否生效,既然這樣,我們前面說過了,Mockito的兩個重要的功能是,驗證Mock對象的方法的調用和可以指定mock對象的某些方法的行為。我們是否可以Mock一個Context對象并且指定它讀取String.xml的行為?答案是可以的,如下就是使用Mockito的一段測試代碼

    public class TestListFilter2 {
        @Mock
        Context mContext;
        @Mock
        Resources mResources;
    
        @Before
        public void setup() {
            MockitoAnnotations.initMocks(this);
            Mockito.when(mContext.getResources()).thenReturn(mResources);
            Mockito.when(mResources.getStringArray(R.array.default_core_list)).thenReturn(getEssentialProcessArray());
            //模擬加載XML資源
            EssentialProcessFilter.loadCoreList(mContext);
        }
    
        /**
         * 測試關鍵服務的過濾器
         */
        @Test
        public void testEssentialFilter() {
            EssentialProcessFilter processFilter = new EssentialProcessFilter();
            ListFilter<RunningAppBean> listFilter = Mockito.spy(ListFilter.class);
            listFilter.addFilter(processFilter);
            List<RunningAppBean> list = new ArrayList<RunningAppBean>();
            list.addAll(getEssentialAppBean());
            list.addAll(getNormalRunningApp());
            List<RunningAppBean> result = Mockito.mock(ArrayList.class);
            for (RunningAppBean runningAppBean : list) {
                if (listFilter.accept(runningAppBean)) {
                    result.add(runningAppBean);
                }
            }
            Mockito.verify(listFilter, Mockito.times(list.size())).accept(Mockito.any(RunningAppBean.class));
            Mockito.verify(result, Mockito.times(getNormalRunningApp().size())).add(Mockito.any(RunningAppBean.class));
        }
        /**
        * 關鍵服務應用包名
        */
        public String[] getEssentialProcessArray() {
          return new String[]{"android.process.acore", "android.process.media", "android.tts", "android.uid.phone", "android.uid.shared", "android.uid.system"};
        }
      }

    上面的代碼,我們使用@Mock來Mock了Context和Resource對象,這需要我們在setup的時候使用MockitoAnnotations.initMocks(this)方法來使得這些注解生效,如果再不使用@Mock注解的時候,我們還可以使用Mockito.mock方法來Mock對象。這里我們指定了Context對象在調用getResources方法的時候返回一個同樣是Mock的Resources對象,這里的Resources對象,指定了在調用getStringArray(R.array.default_core_list)方法的時候返回的字符串數組的數據是通過我們的getEssentialProcessArray方法獲得的,而不是真的是加載String.xml資源。最后調用EssentialProcessFilter.loadCoreList(mContext)方法使得EssentialProcessFilter內記錄的關鍵服務集合的數據源就是我們指定的。目前,我們使用的就是改變Mock對象的行為的功能。

    在測試單元testEssentialFilter方法中,使用Mockito.spy(ListFilter.class)來Mock一個ListFilter對象(這是一個BaseItemFilter的實現,里面記錄了BaseItemFilter的集合,用了記錄一系列的過濾規則),這里使用spy方法Mock出來的對象除非指定它的行為,否者調用這個對象的默認實現,而使用mock方法Mock出來的對象,如果不指定對象的行為的話,所有非void方法都將返回默認值:int、long類型方法將返回0,boolean方法將返回false,對象方法將返回null等等,我們也同樣可以使用@spy注解來Mock對象。這里的listFilter對象使用spy是為了使用默認的行為,確保accept方法的邏輯正確執行,而result對象使用mock方式來Mock,是因為我們不需要真的把過濾后的數據添加到集合中,而只需要驗證這個Mock對象的add方法調用的多少次即可。

    最后就是對Mock對象的行為的驗證,分別驗證了listFilter#accept方法和result#add方法的執行次數,其中Mockito#any系列方法用來指定無論傳入任何參數值。

    依賴注入,寫出更容易測試的代碼

    這里舉一個新的例子,在使用MVP模式開發一個登錄頁

    這是我們的LoginPresenter,Presenter的職責是作為ViewModel之間的橋梁,UserManager就是這里的Model層,它用來處理用戶登錄的業務邏輯

    public class LoginPresenter {
        private final LoginView mLoginView;
        private UserManager mUserManager = new UserManager();
    
        public LoginPresenter(LoginView loginView) {
              mLoginView = loginView;
        }
    
        public void login(String username, String password) {
            //....
            mLoginView.showLoginHint();
            mUserManager.performLogin(username, password);
            //...
        }

    這段代碼存在了一些問題

    • 如果現在要改變UserManager生成方式,如需要用new UserManager(String config)初始化,需要修改LoginPresenter代碼
    • 如果想測試不同UserManager對象對LoginPresenter的影響很困難,因為UserManager的初始化被寫死在了LoginPresenter的構造函數中(現在的Mockito可以使用@InjectMocks來很大程度緩解這個問題)

    為了把依賴解耦,我們一般可以作如下改變

    public class LoginPresenter {
        private final LoginView mLoginView;
        private final UserManager mUserManager;
    
        //將UserManager作為構造方法參數傳進來
        public LoginPresenter(LoginView loginView,UserManager userManager) {
            this.mLoginView = loginView;
            this.mUserManager = userManager;
        }
    
        public void login(String username, String password) {
            //... some other code
            mUserManager.performLogin(username, password);
        }
    }

    這就是一種常見的依賴注入方式,這種方式的好處是,依賴關系非常明顯。你必須在創建這個類的時候,就提供必要的dependency。這從某種程度上來說,也是在說明這個類所完成的功能。實現依賴注入很簡單,比較有名的Dagger、Dragger2這些框架可以讓這種實現變得更加簡單,簡潔,優雅,有興趣可以自行了解。 作出這種修改之后,我們就可以很容易的Mock出UserManager對象來對LoginPresenter做單元測試

    對異步任務進行測試

    很多時候,我們耗時的業務邏輯都是再異步線程中處理的,這樣就會對我們的測試造成了一些困難,因為很可能我們的異步任務執行完之前,我們的Test單元已經執行完了,為了能夠順利驗證,其中一個思路是,首先需要一個回調接口來告訴我們處理完成,之后再我們知道回調后,繼續執行測試代碼,這里的一個簡單實現就是使用CountDownLatch,也可以使用waitnotifyAll,下面以CountDownLatch為例子

    這里是我們的回調接口

    public interface ScanListener<T> {
        /**
         * @param t 掃描結果
         */
        void onScannedCompleted(T t);
    }

    測試代碼如下:

    public class TestRunningAppScanner {
        CountDownLatch mSignal = null;
        RunningAppScanner mRunningAppScanner;
        @Mock
        BaseScanner.ScanListener<List<RunningAppBean>> mRunningAppBeanScanListener;
        @Captor
        ArgumentCaptor<List<RunningAppBean>> mListArgumentCaptor;
    
        @Before
        public void setUp() {
            mSignal = new CountDownLatch(1);
            MockitoAnnotations.initMocks(this);
            mRunningAppScanner = Mockito.spy(new RunningAppScanner(InstrumentationRegistry.getTargetContext()));
        }
    
        @After
        public void tearDown() {
            mSignal.countDown();
        }
    
        @Test
        public void testRunningApp() throws InterruptedException {
            Assert.assertFalse(mRunningAppScanner.isRunning());
            Mockito.doAnswer(new Answer() {
                @Override
                public Object answer(InvocationOnMock invocation) throws Throwable {
                    mSignal.countDown();
                    return invocation.getArguments();
                }
            }).when(mRunningAppBeanScanListener).onScannedCompleted((List<RunningAppBean>) Mockito.any());
            mRunningAppScanner.setScanListener(mRunningAppBeanScanListener);
            mRunningAppScanner.startScanning();
            Assert.assertTrue(mRunningAppScanner.isRunning());
            mSignal.await();
            Assert.assertFalse(mRunningAppScanner.isRunning());
            Mockito.verify(mRunningAppBeanScanListener, Mockito.times(1)).onScannedCompleted(mListArgumentCaptor.capture());
            //必定最少有1個進程再運行
            Assert.assertTrue(mListArgumentCaptor.getValue().size() > 0);
        }
      }

    上面代碼中Mock了一個ScanListener類型的回調接口,并在testRunningApp方法中,指定了它onScannedCompleted方法的行為,RunningAppScanner#startScanning方法是用來啟動異步邏輯的,在開始異步邏輯之前設置了回調接口,在開始異步邏輯后,就使用了mSignal.await()方法來阻塞當前線程,直到onScannedCompleted方法的執行,會調用到在setup的時候新建的CountDownLatch對象的countDown(),表明異步任務的完成,以繼續執行下面的測試代碼

    上面的測試代碼實際上是屬于Instrumented Tests,它的異步邏輯是去掃描當前設備運行的進程,需要運行再Android設備上,它的異步邏輯是實際運行的而且會掃描出真實結果。但有些時候,我們只是想驗證狀態和交互結果,而不需要它真的執行異步邏輯,我們可以使用其他的方式來進行測試,我們以一個登錄流程為例子

    這是登錄流程的時序圖


    Login Sequence

    回調的定義

    public interface LoginCallback {
    
        void onSuccess();
    
        void onFail(int code);
    }

    這是LoginPresenter

    public class LoginPresenter {
        private final LoginView mLoginView;
        private final UserManager mUserManager;
    
        //將UserManager作為構造方法參數傳進來
        public LoginPresenter(LoginView loginView, UserManager userManager) {
            this.mLoginView = loginView;
            this.mUserManager = userManager;
        }
    
        public void login(String username, String password) {
            mLoginView.showLoginHint();
            mUserManager.performLogin(username, password, new UserManager.LoginCallback() {
                @Override
                public void onSuccess() {
                    mLoginView.showSuccess();
                }
    
                @Override
                public void onFail(int code) {
                    mLoginView.showError();
                }
            });
        }
    }

    我們現在需要對LoginPresenter進行測試,我們的測試點在于接收到不同的回調結果的時候,對View進行不同的展示,至于UserManager的測試則應該是另外的一個測試單元的 LoginPresenter的測試代碼

    public class TestDemo {
        @Mock
        LoginView mLoginView;
        @Mock
        UserManager mUserManager;
    
        LoginPresenter mLoginPresenter;
    
        @Captor
        ArgumentCaptor<LoginCallback> mCallbackArgumentCaptor;
    
        @Before
        public void before() {
            MockitoAnnotations.initMocks(this);
            mLoginPresenter = new LoginPresenter(mLoginView, mUserManager);
        }
    
        @Test
        public void showError() {
            mLoginPresenter.login("username", "password");
            Mockito.verify(mUserManager, Mockito.times(1)).performLogin(Mockito.anyString(), Mockito.anyString(), mCallbackArgumentCaptor.capture());
            mCallbackArgumentCaptor.getValue().onFail(0);
            Mockito.verify(mLoginView, Mockito.times(1)).showError();
        }
    
        @Test
        public void showSuccess() {
            mLoginPresenter.login("username", "password");
            Mockito.verify(mUserManager, Mockito.times(1)).performLogin(Mockito.anyString(), Mockito.anyString(), mCallbackArgumentCaptor.capture());
            mCallbackArgumentCaptor.getValue().onSuccess();
            Mockito.verify(mLoginView, Mockito.times(1)).showSuccess();
        }
    }

    這里的關鍵是@Captor ArgumentCaptor<LoginCallback> mCallbackArgumentCaptor; ,ArgumentCaptor是作用是用來捕獲所傳入方法特定的參數,然后可以進一步對參數進行斷言,而且是在方法調用之后使用。這里我們捕獲到UserManager#performLogin的第三個LoginCallback的參數,然后直接使用它來做相應回調來驗證不同情況下的反饋結果是否正確

    Mockito的一些替代或擴增庫

    使用Mockito并不可以Mock對象的靜態方法、private修飾的方法、static方法、構造函數等,使用JMockit或PowerMock是可以解決這樣的問題,有時間的話可以去實踐下

    Local Unit Tests的優點

    • 不依賴Android的API,運行速度快,所以更快地得到結果反饋
    • 引導更好的代碼設計(單一職責、依賴注入),如果一個類不好測,往往是因為這個類的設計是有問題

    Instrumented Tests

    Instrumented Unit tests是需要運行再Android設備上的(物理/虛擬),通常我們使用Mock的方式不能很好解決對Android的API的依賴的這個問題,而使用這種測試方式可以依賴Android的API,使用Android提供的Instrumentation系統,將單元測試代碼運行在模擬器或者是真機上,但很多情況來說,我們還是會需要和Mockito一起使用的。這中方案速度相對慢,因為每次運行一次單元測試,都需要將整個項目打包成apk,上傳到模擬器或真機上,就跟運行了一次app似得,所以。

    配置

    • 測試代碼目錄:module-name/src/androidTests/java/
    • 一般使用到的測試框架

      • AndroidJUnitRunner : JUnit 4-compatible test runner for Android
      • Espresso :UI testing framework; suitable for functional UI testing within an app
      • UI Automator :UI testing framework; suitable for cross-app functional UI testing across system and installed apps

    通過Gralde添加相應的庫

    dependencies {
        androidTestCompile 'com.android.support:support-annotations:24.0.0'
        androidTestCompile 'com.android.support.test:runner:0.5'
        androidTestCompile 'com.android.support.test:rules:0.5'
        // Optional -- Hamcrest library
        androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
        // Optional -- UI testing with Espresso
        androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
        // Optional -- UI testing with UI Automator
        androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
    }

    另外還需要在你的App的模塊的build.gralde文件添加如下設置:

    android {
        defaultConfig {
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }

    使用

    AndroidJUnitRunner

    AndroidJUnitRunner是JUnit4運行在Android平臺上的兼容版本,使用也是很簡單的且方法類似,以上面測試異步任務為例子

    @RunWith(AndroidJUnit4.class)
    @MediumTest
    public class TestRunningAppScanner {
        CountDownLatch mSignal = null;
        RunningAppScanner mRunningAppScanner;
        @Mock
        BaseScanner.ScanListener<List<RunningAppBean>> mRunningAppBeanScanListener;
    
    
        @Before
        public void setUp() {
            mSignal = new CountDownLatch(1);
            MockitoAnnotations.initMocks(this);
            mRunningAppScanner = Mockito.spy(new RunningAppScanner(InstrumentationRegistry.getTargetContext()));
        }
    
        @After
        public void tearDown() {
            mSignal.countDown();
        }
    
        @Test
        public void testRunningApp() throws InterruptedException {
            Assert.assertFalse(mRunningAppScanner.isRunning());
            Mockito.doAnswer(new Answer() {
                @Override
                public Object answer(InvocationOnMock invocation) throws Throwable {
                    mSignal.countDown();
                    return invocation.getArguments();
                }
            }).when(mRunningAppBeanScanListener).onScannedCompleted((List<RunningAppBean>) Mockito.any());
            mRunningAppScanner.setScanListener(mRunningAppBeanScanListener);
            mRunningAppScanner.startScanning();
            Assert.assertTrue(mRunningAppScanner.isRunning());
            mSignal.await();
            Assert.assertFalse(mRunningAppScanner.isRunning());
            Mockito.verify(mRunningAppBeanScanListener, Mockito.times(1)).onScannedCompleted((List<RunningAppBean>) Mockito.any());
        }
      }

    需要在測試類的定義上加上@RunWith(AndroidJUnit4.class)標注,另外@MediumTest標注是用來指定測試規模,有三種類型可以指定,限制如下,因為這里的異步任務的目的是掃描所有正在運行的App,所以這里使用@MediumTest,其他的Assert#XXX、@Befor,@After使用一致,另外這里還搭配Mockito使用。因為需要真正用到系統的一些服務(AMS,PKMS)或資源,這里的InstrumentationRegistry可以為我們提供Instrumentation對象,測試App的Context對象,目標App的Context對象和通過命令行啟動測試所提供的參數信息。這里的 InstrumentationRegistry.getTargetContext() 就是用來獲取目標App的Context對象。

    Feature SmallTest MediumTest LargeTest
    Network No localhost only Yes
    Database No Yes Yes
    File system access No Yes Yes
    Use external systems No Discouraged Yes
    Multiple threads No Yes Yes
    Sleep statements No Yes Yes
    System properties No Yes Yes
    Execution Time (ms) 200< 1000< >1000

    Espresso

    Espresso提供大量的方法用來進行UI測試,這些API可以讓你寫的簡潔和可靠的自動化UI測試,站在用戶的角度測試,所有邏輯是黑盒

    Espresso的三個重要功能:

    使用流程

    • 元素定位
    public static ViewInteraction onView(final Matcher<View> viewMatcher) {}

    Espresso.onView方法接收一個Matcher<View>類型的入參,并返回一個ViewInteraction對象。ViewMatchers對象提供了大量的withXXX方法用來定位元素,常用的有withId,withText和一系列的isXXX方法用來判斷元素的狀態。如果單一的匹配條件無法精確地匹配出來唯一的控件,我們可能還需要額外的匹配條件,此時可以用AllOf#allOf()方法來進行復合匹配條件的構造(下面的AdapterView節有使用到):

    onView(allOf(withId(id), withText(text)))
    • 操作元素

    當定位到元素后,返回一個ViewInteraction對象,其perform方法可以接收一系列ViewAction用來進行模擬用戶操作,ViewActions類提供了大量的操作實現,常用的有typeText,click

    public ViewInteraction perform(final ViewAction... viewActions) {}
    • 驗證結果

    最后為了驗證操作是否符合預期,我們還是需要定位到元素,獲得一個ViewInteraction對象,其check方法接收了一個ViewAssertion的入參,該入參的作用就是檢查結果是否符合我們的預期。

    public ViewInteraction check(final ViewAssertion viewAssert) {}

    ViewAssertion提供如下方法,這個方法接收了一個匹配規則,然后根據這個規則為我們生成了一個ViewAssertion對象,這個Matcher<View>的如參和定位元素的時候是一個用法的

    public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {}

    下面是測試主頁部分UI測試的代碼

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class MainActivityUITest {
        /**
         * {@link ActivityTestRule} is a JUnit {@link Rule @Rule} to launch your activity under test.
         * <p/>
         * Rules are interceptors which are executed for each test method and are important building
         * blocks of Junit tests.
         */
         @Rule
         public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class, true, false);
    
         //默認在測試之前啟動該Activity
     //    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class);
    
    
        @Test
        public void testNavigateToAdvanceFragment() {
            Intent intent = new Intent();
            //攜帶信息的方式啟動Activity
            intent.putExtra("EXTRA", "Test");
            mActivityRule.launchActivity(intent);
            // Open Drawer to click on navigation.
            Espresso.onView(ViewMatchers.withId(R.id.drawer_layout))
                    .check(ViewAssertions.matches(DrawerMatchers.isClosed(Gravity.LEFT)))
                    .perform(DrawerActions.open()); // Open Drawer
            // navigate to advance fragment.
            Espresso.onView(ViewMatchers.withText(R.string.advanced_settings_group_title_advanced_features))
                    .perform(ViewActions.click());
            // Left Drawer should be closed.
            Espresso.onView(ViewMatchers.withId(R.id.drawer_layout))
                    .check(ViewAssertions.matches(DrawerMatchers.isClosed(Gravity.LEFT)));
            Espresso.onView(ViewMatchers.withText(R.string.fingerprint_settings_title)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
        }
    
    }

    @Rule+ActivityTestRule用來標記需要測試的Activity,測試框架會再運行@Test標注的測試用例之前會啟動這個指定的Activity(類似還有IntentsTestRule,ServiceTestRule分別用于測試Intent和Service),有些情況我們測試的Actvity需要再Intent那里攜帶一些信息,這里也是可以通過ActivityTestRule的不同構造函數+ActivityTestRule#launchActivity方法來完成

    它的作用是找到R.id.drawer_layout的View,這是一個DrawerLayout,DrawerMatchers提供了用于判斷DrawerLayout的狀態,DrawerActions類提供了用于操作DrawerLayout的操作。首先是判斷DrawerLayout是隱藏的,然后打開抽屜,并找到R.string.advanced_settings_group_title_advanced_features標記的Item,進行一次點擊操作,最后驗證當前顯示的是高級界面

    處理AdapterView

    不同于之前提到的靜態的控件,AdapterView在加載數據時,可能只有一部分顯示在了屏幕上,對于沒有顯示在屏幕上的那部分數據,我們通過Espresso.onView()是沒有辦法找到的。所以需要使用Espresso.onData(),為了精確定位,通常還需要我們自定義Matcher

    基本流程的區別在于元素定位上:

    • 元素定位
    public static DataInteraction onData(Matcher<? extends Object> dataMatcher) {}

    這個方法接收一個Matcher<? extends Object>類型的入參,并返回一個DataInteraction對象。DataInteraction關注于AdapterView的數據。由于AdapterView的數據源可能很長,很多時候無法一次性將所有數據源顯示在屏幕上,因此我們主要先關注AdapterView中包含的數據,而非一次性就進行視圖元素的匹配,入參Matcher<? extends Object>構造一個針對于Object(數據)匹配的匹配規則。從以上對比可以看出,我們在使用onData()方法對AdapterView進行測試的時候,我們的思路就轉變成了首先關注這個AdapterView的具體數據,而不是UI上呈現的內容。當然,我們最終的目標還是要找到目標的UI元素,但是我們是通過其數據源來進行入手的

    • 自定義Matcher

      對于針對于Object(數據)的匹配器一般都需要我們自定義的,如下是針對AdapterView的數據是LockerItem類型的匹配器,用于LockerItem對象的title字段是否為輸入的字符串,復寫的matchesSafely()方法是真正執行匹配的地方了:

      /**
      * 查找指定關鍵字的搜索條件
      * @param name 需要搜索的關鍵字
      */
      public static Matcher<Object> lockItemMatcher(final String name) {
         return new BoundedMatcher<Object, LockerItem>(LockerItem.class) {
             @Override
             protected boolean matchesSafely(LockerItem item) {
                 return item != null && item.getTitle().equals(name);
               }
      
               @Override
               public void describeTo(Description description) {
                 description.appendText("SearchItem has Name: " + name);
               }
             };
           }
    • 指定AdapterView

      通常還需要我們指定AdapterView,避免存在多個AdapterView的時候所帶來的麻煩,通過inAdapterView方法來匹配,匹配規則和之前的視圖匹配一樣,可以同id來進行匹配

      public DataInteraction inAdapterView(Matcher<View> adapterMatcher){}
    • Child View

      有時候我們需要對一個View中的某個子控件進行操作(比如點擊一個ListView條目中的某個指定Button),這時我們可以通過`onChildView``方法指定相應的子控件

      DataInteraction onChildView(Matcher<View> childMatcher) {}
    • 操作元素

    同處理普通元素差不多

    • 驗證結果

    同處理普通元素差不多

    下面是一個找到id為R.id.main_list和數據類型為LockerItem的AdapterView中標題為WeChat的元素,并對該元素中id為R.id.item_ripple的子元素模擬一次點擊事件

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class MainActivityUITest {
        /**
         * {@link ActivityTestRule} is a JUnit {@link Rule @Rule} to launch your activity under test.
         * <p/>
         * Rules are interceptors which are executed for each test method and are important building
         * blocks of Junit tests.
         */
        @Rule
        public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class, true, false);
    
        @Test
        public void testLockFirstItem() throws InterruptedException {
            Intent intent = new Intent();
            intent.putExtra("EXTRA", "Test");
            mActivityRule.launchActivity(intent);
            onData(allOf(is(instanceOf(LockerItem.class)),lockItemMatcher("WeChat"))).inAdapterView(withId(R.id.main_list)).onChildView(withId(R.id.item_ripple)).perform(click());
            //...
        }
    
        /**
         * 查找指定關鍵字的搜索條件
         * @param name 需要搜索的關鍵字
         */
        public static Matcher<Object> lockItemMatcher(final String name) {
            return new BoundedMatcher<Object, LockerItem>(LockerItem.class) {
                @Override
                protected boolean matchesSafely(LockerItem item) {
                    return item != null && item.getTitle().equals(name);
                }
    
                @Override
                public void describeTo(Description description) {
                    description.appendText("SearchItem has Name: " + name);
                }
            };
        }
    
    }

    MVP各層的單元測試框架的使用

    Google官方的Android Architecture Blueprints這個項目除了是Google利用不同的流行的開源庫組合來搭建MVP框架的實踐,同時還有完整的測試用例,所以學習價值很高,所以如果沒了解過,現在就去看看吧,必定受益匪淺

    這是這個項目對于MVP模式的各層做測試的各種測試框架使用


    MVPTesting
    • View層:涉及到UI且需要再設備上運行,所以需要Espresso和AndroidJUnitRunner
    • Preseneter層:Preseneter層應該設計成純JAVA層的,所以使用JUnit+Mockito
    • Model層:需要依賴Android環境

     

    原文轉自:http://www.jianshu.com/p/00ab03f3d394

    老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月

  • <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>