Android单元测试的整理

作者: kachidokima | 来源:发表于2017-06-09 11:51 被阅读492次

    也是以前就整理,最近在强化练习,所以顺便系统整理了一下,测试的话,随着项目便的复杂个人觉得越来越重要,还是偏向于使用,我也不深究原理了。
    最近实践,个人比较喜欢采用JUit+Mock+Espresso,所以也就展示了这三个。本来想分篇的,最后还是压缩了一下就一篇吧。
    文中代码大部分是以前摘录的,比较零散也忘记出处了,也有自己写的一些,总体来说都是比较好的示例。

    JUnit

    导包

    //如果只在Java环境下测试,只需以下且默认都有这个配置
    testCompile 'junit:junit:4.12'
    
    //如果需要调用Android的组件则需要多加
    androidTestCompile 'com.android.support.test:runner:0.5' 
    androidTestCompile 'com.android.support:support-annotations:'+supportLibVersion
    //且defaultConfig节点需要加上
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    

    使用

    这是Java界用的最广泛,很多框架都是基于这个框架的,用起来也比较简单

    public int add(int one, int another) {
       return one + another;
    }
    

    本来写个测试需要这样:

    public int test() {
        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!")
        }
    }
    

    现在有了这个框架,只需要这样

    //会在每个测试方法前执行
    @Before
    public void setup() {
      mCalculator = new Calculator();
    }
    
    @Test
    public void testAdd() throws Exception {
      int sum = calculator.add(1, 2);
      Assert.assertEquals(3, sum);
    }
    //如果要验证抛出异常
    @Test(expected = IllegalArgumentException.class)
    public void test() {
      mCalculator.divide(4, 0);
    }
    

    验证方法都在Assert类中,看方法名就能理解,不列举了

    然后说一下常用的注解:

    • setUp/@Before:在每个单元测试方法执行之前调用
    • tearDown/@After:在每个单元测试方法执行后调用
    • setUpBeforeClass/@BeforeClass:在每个单元测试类运行前调用
    • tearDownAfterClass/@AfterClass:在每个单元测试类运行完成后调用
    • Junit3中每个测试方法必须以test打头,Junit4中增加了注解,对方法名没有要求,@Test就可以
    • 如果想在测试类中暂时忽略某个方法,标注@Ignore

    高阶

    Parameterized

    主要用于多参数的一次性测试,需要5步

    • @RunWith(Parameterized.class) 来注释 test 类
    • 创建一个由 @Parameters 注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合
    • 创建一个公共的构造函数,它接受和一行测试数据相等同的东西
    • 为每一列测试数据创建一个实例变量
    • 用实例变量作为测试数据的来源来创建你的测试用例

    示例:

    @RunWith(Parameterized.class)
    public class CalculatorAddParameterizedTest {
        @Parameters
        public static Iterable<Object[]> data() {
            return Arrays.asList(new Object[][]{
                    {0, 0, 0},
                    {0, -1, -1},
                    {2, 2, 4},
                    {8, 8, 16},
                    {16, 16, 32},
                    {32, 0, 32},
                    {64, 64, 128}});
        }
    
        private final double mOperandOne;
        private final double mOperandTwo;
        private final double mExpectedResult;
    
        private Calculator mCalculator;
      
        public CalculatorAddParameterizedTest(double operandOne, double operandTwo,
                double expectedResult) {
            mOperandOne = operandOne;
            mOperandTwo = operandTwo;
            mExpectedResult = expectedResult;
        }
    
        @Before
        public void setUp() {
            mCalculator = new Calculator();
        }
    
        @Test
        public void testAdd_TwoNumbers() {
            double resultAdd = mCalculator.add(mOperandOne, mOperandTwo);
            assertThat(resultAdd, is(equalTo(mExpectedResult)));
        }
    }
    

    Rule

    类似于@Before@After,是用来在每个测试方法的执行前后可以标准化的执行一些代码,一般我们直接用框架中现有的Rule就可以了

    具体可以看这篇:

    Junit Rule的使用

    Mockito

    依赖

    androidTestCompile "org.mockito:mockito-core:$mockitoVersion"
    

    Mock作用

    • 专注于单元测试,可以把想要测试类中没有实现的模块虚拟Mock出来,先给需要测试的模块用着
    • Mock出来的类是空壳,是一个继承与原类,方法都是hook的新类,每个方法都需要Stub,否则返回的都是默认值
    • Spy出来的类可以使用原来类的方法,但是也可以指定方法有hook处理

    使用

    创建Mock

    使用Rule

    @Mock
    MyDatabase databaseMock; 
    @Rule 
    public MockitoRule mockitoRule = MockitoJUnit.rule(); 
    

    标注RunWith

    @RunWith(MockitoJUnitRunner.class)
    public class Test{
      @Mock
      MyDatabase databaseMock; 
    }
    

    手动mock

    MyClass test = mock(MyClass.class);
    

    指定Stub

    创建之后由于都是空的,所以要指定行为

    若方法中的某一个参数使用了matcher,则所有的参数都必须使用matcher

    第一种:Mockito.when(obj.methodCall()).thenReturn(result)

    不能用于重复的Stub、返回void函数、Spy出来的类

    @Test
    public void test1()  {
        // define return value for method getUniqueId()
        when(test.getUniqueId()).thenReturn(43);
        // use mock in test....
        assertEquals(test.getUniqueId(), 43);
    }
    
    
    @Test
    public void testMoreThanOneReturnValue()  {
        Iterator<String> i= mock(Iterator.class);
        when(i.next()).thenReturn("Mockito").thenReturn("rocks");
        String result= i.next()+" "+i.next();
        //assert
        assertEquals("Mockito rocks", result);
    }
    
    @Test
    public void testReturnValueInDependentOnMethodParameter()  {
        Comparable<Integer> c= mock(Comparable.class);
        when(c.compareTo(anyInt())).thenReturn(-1);
        //assert
        assertEquals(-1, c.compareTo(9));
    }
    
    //return都可以用answer来代替
    @Test
    public final void answerTest() {
        // with thenAnswer():
        when(list.add(anyString())).thenAnswer(returnsFirstArg());
        // with then() alias:
        when(list.add(anyString())).then(returnsFirstArg());
    }
    

    但是如果用这个来指定Spy则无效

    @Test
    public void testLinkedListSpyWrong() {
        // Lets mock a LinkedList
        List<String> list = new LinkedList<>();
        List<String> spy = spy(list);
        //无效且会抛出异常,因为调用了一次方法且此时list空
        when(spy.get(0)).thenReturn("foo");
        assertEquals("foo", spy.get(0));
    }
    

    第二种:Mockito.doReturn(result).when(obj).methodCall()

    可以重复Stub,可以使用doAnswer来Stub方法

    @Test
    public void testLinkedListSpyCorrect() {
        // Lets mock a LinkedList
        List<String> list = new LinkedList<>();
        List<String> spy = spy(list);
    
        // You have to use doReturn() for stubbing
        doReturn("foo").when(spy).get(0);
    
        assertEquals("foo", spy.get(0));
    }
    
        // with doAnswer():
        doAnswer(returnsFirstArg()).when(list).add(anyString());
    

    when(….).thenReturn(….)方法链可以用于抛出异常

    Properties properties = mock(Properties.class);
    when(properties.get(”Anddroid”)).thenThrow(new IllegalArgumentException(...));
    try {
        properties.get(”Anddroid”);
        fail(”Anddroid is misspelled”);
    } catch (IllegalArgumentException ex) {
        // good!
    }
    

    并且可以指定Spy

    @Test
    public void testLinkedListSpyCorrect() {
        // Lets mock a LinkedList
        List<String> list = new LinkedList<>();
        List<String> spy = spy(list);
        // You have to use doReturn() for stubbing
        doReturn("foo").when(spy).get(0);
    
        assertEquals("foo", spy.get(0));
    }
    

    doAnswer

    //需要测试的代码
    public void getTasks(@NonNull final LoadTasksCallback callback) {...}
    interface LoadTasksCallback {
      void onTasksLoaded(List<Task> tasks);
      void onDataNotAvailable();
    }
    
    //stub
    doAnswer(new Answer() {
                @Override
                public Object answer(InvocationOnMock invocation) throws Throwable {
                    Object[] arg=invocation.getArguments();//获取参数
                    TasksDataSource.LoadTasksCallback callback = (TasksDataSource.LoadTasksCallback) arg[0];//0代表第一个参数
                    callback.onTasksLoaded(TASKS);
                    return null;
                }
            }).when(mTasksRepository).getTasks(any(TasksDataSource.LoadTasksCallback.class));
    

    验证测试

    主要验证是否方法调用和次数

    //方法调用,且参数一定
    Mockito.verify(mockUserManager, Mockito.times(2)).performLogin("xiaochuang", "xiaochuang password");
    //如果是一次,可以简写
    Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
    //也可以限定次数
    Mockito.verify(test, atLeastOnce()).someMethod("called at least once");
    

    高阶

    ArgumentCaptor

    可以捕获方法的参数,然后进行验证,也可以用来在有回调的方法上,避免doAnswer的复杂写法

    需要导包 hamcrest-library

    @Captor
    private ArgumentCaptor<List<String>> captor;
    @Test
    public final void shouldContainCertainListItem() {
        List<String> asList = Arrays.asList("someElement_test", "someElement");
        final List<String> mockedList = mock(List.class);
        mockedList.addAll(asList);
    
        verify(mockedList).addAll(captor.capture());
        final List<String> capturedArgument = captor.getValue();
        assertThat(capturedArgument, hasItem("someElement"));
    }
    

    InOrder

    可以指定验证的次序

    // A. Single mock whose methods must be invoked in a particular order  
    List singleMock = mock(List.class);  
      
    //using a single mock  
    singleMock.add("was added first");  
    singleMock.add("was added second");  
      
    //create an inOrder verifier for a single mock  
    InOrder inOrder = inOrder(singleMock);  
      
    //following will make sure that add is first called with "was added first, then with "was added second"  
    inOrder.verify(singleMock).add("was added first");  
    inOrder.verify(singleMock).add("was added second");  
      
    // B. Multiple mocks that must be used in a particular order  
    List firstMock = mock(List.class);  
    List secondMock = mock(List.class);  
      
    //using mocks  
    firstMock.add("was called first");  
    secondMock.add("was called second");  
      
    //create inOrder object passing any mocks that need to be verified in order  
    InOrder inOrder = inOrder(firstMock, secondMock);  
      
    //following will make sure that firstMock was called before secondMock  
    inOrder.verify(firstMock).add("was called first");  
    inOrder.verify(secondMock).add("was called second");  
      
    // Oh, and A + B can be mixed together at will  
    

    @InjectMocks

    主动构造有构造函数的Mock,且其参数也需要用注解来生成

    public ArticleManager(User user, ArticleDatabase database) {
        super();
        this.user = user;
        this.database = database;
    }
    
    @RunWith(MockitoJUnitRunner.class)
    public class ArticleManagerTest  {
        @Mock 
        ArticleDatabase database;
        @Mock 
        User user;
        @InjectMocks 
        private ArticleManager manager; 
    }
    

    Espresso来UI测试

    依赖

    // Android JUnit Runner
    androidTestCompile 'com.android.support.test:runner:0.5'
    // JUnit4 Rules
    androidTestCompile 'com.android.support.test:rules:0.5'
     //一些依赖关系可能出现的冲突。在这种情况下可以在 espresso-contrib 中 exclude他们
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2', {
            exclude group: 'com.android.support', module: 'support-annotations'
    }
    
    //并且需要在 defaultConfig 节点添加
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    

    并且每个测试类都需要加标注

    @RunWith(AndroidJUnit4.class)
    public class Test {...}
    

    ActivityTestRule

    在开始自动化测试之前都会定义一个ActivityTestRule 它用于在测试的时候launch待测试的activity

    获得View

    使用ViewMatcher.class里面的方法可以找到你想要的View,如你想找有Hello文字的View,你可以这样使用

     onView(withText("Hello"));
    

    相似的你也可以使用View的资源Id来找到该view

    onView(withId(R.id.hello));
    

    当有多个约束条件时,可以使用Matchers.class的allof()方法来组合,例子如下:

    onView(allOf(withText("Hello") ,withId(R.id.hello)));
    

    对View执行一些操作

    对View操作的代码大概是这样: onView(...).perform();
    在onView中找到这个View后,调用perform()方法进行操作,如点击该View:

    onView(withId(R.id.hello)).perform(click());
    

    也可以执行多个操作在一个perform中如

    onView(withId(R.id.hello)).perform(click(),clearText());
    

    检查View(测试与验证)

    使用check()方法来检查View是否符合我们的期望 onView(...).check();
    如检查一个View里面是否有文字Hello:

    onView(withId(R.id.hello)).check(matches(withText("Hello")));
    

    总之全部操作都在这个图里了

    image

    其他

    1. 判断这个View存不存在,返回一个boolen

      //Espresso不推荐在测试使用条件逻辑,找不到而不想直接报错只能try catch
      try {
              onView(withText("my button")).check(matches(isDisplayed()));
              //view is displayed logic
          } catch (NoMatchingViewException e) {
              //view not displayed logic
          }
      
    2. 模拟退出Activity的返回操作

      Espresso.pressBack();
      
    3. 有2个一样文字View,怎么只使用第一次找到的这个View

      public static  <T> Matcher<T> firstFindView(final Matcher<T> matcher) {
              return new BaseMatcher<T>() {
                  boolean isFirst = true;
      
                  @Override
                  public boolean matches(final Object item) {
                      if (isFirst && matcher.matches(item)) {
                          isFirst = false;
                          return true;
                      }
                      return false;
                  }
      
                  @Override
                  public void describeTo(final Description description) {
                      description.appendText("should return first matching item");
                  }
              };
          }
      //使用
      onView(allOf(isDisplayed(),firstFindView(withText("Hello"))));
      

    高阶

    Intented与Intending

    导包

    androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2'
    

    每次使用必须先Intents.init(),用完后必须调用Intents.release释放,或者使用的RuleActivityIntentsTestRule,比如:

    //继承自ActivityTestRule,会在每个测试前和结束自动初始化和释放
    @Rule
    public IntentsTestRule<ImageViewerActivity> mIntentsRule = new IntentsTestRule<>(ImageViewerActivity.class);
    

    使用

    Intending与Mockito.when相似,respondWith 相当于 thenReturn

     ActivityResult result = new ActivityResult(Activity.RESULT_OK, resultData);
     intending(hasComponent(hasShortClassName(".ContactsActivity"))).respondWith(result);
    

    Intenteded与Mockito.verify相似,验证某个Intent是否发出

    intended(allOf(
                    hasAction(Intent.ACTION_CALL),
                    hasData(INTENT_DATA_PHONE_NUMBER),
                    toPackage(PACKAGE_ANDROID_DIALER)));
    

    Espresso中提供了许多方法用于检测Intent的各个部分,下面是每个字段的对应关系

    Intent.setData <–> hasData
    Intent.setAction <–> hasAction
    Intent.setFlag <–> hasFlag
    Intent.setComponent <–> hasComponent
    

    一个标准的使用可以是这样

    public void testLoginPass() {
        ActivityResult activityResult = new ActivityResult(Activity.RESULT_OK, new Intent());
        Intents.init();
        intending(expectedIntent).respondWith(activityResult);
        onView(withId(R.id.button_login)).perform(click());
        intended(expectedIntent);
        Intents.release();
        onView(withId(R.id.button_login)).check(matches(withText(R.string.pass_login)));
    }
    

    其他使用

    想要每个Activity启动的时候都收到某个Intent

    @RunWith(AndroidJUnit4.class)
    public class MainActivityTest {
        @Rule
        public ActivityTestRule<MainActivity> mActivityRule =
                new ActivityTestRule<MainActivity>(MainActivity.class) {
                    @Override
                    protected Intent getActivityIntent() {
                        Context targetContext = InstrumentationRegistry.getInstrumentation()
                            .getTargetContext();
                        Intent result = new Intent(targetContext, MainActivity.class);
                        result.putExtra("Name", "Value");
                        return result;
                    }
                };
    
    }
    

    可以去屏蔽掉其他包发的Intent的影响

    @Before
    public void stubAllExternalIntents() {
       intending(not(isInternal())).respondWith(new ActivityResult(Activity.RESULT_OK, null));
    }
    

    Idling Resource

    一般用在异步里面,可以再测试的时候让其不会因为延迟而导致测试失败

    导包

    compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2' 
    

    为了便于测试,一般都会融合在实际回调中来控制当前是否处于空闲IDLE状态,可以在Activity中加入以下方法,然后再测试中获取

    //要想在测试用例中使用源码中的数据可以使用VisibleForTesting这个注释符
    @VisibleForTesting
    public IdlingResource getIdlingResource() {
        return mIdlingResource;
    }
    

    使用

    首先要实现一个IdlingResource一般app都用一个就可以了,且重写三个函数:

    • getName():必须返回代表idling resource的非空字符串,一般直接通过class.getName()
    • isIdleNow():表示当前是否idle状态
    • registerIdleTransitionCallback(..): 用于注入回调

    然后再有异步可能有延迟的地方使用IdlingResource,一般实现的时候使用Atom类来做并发的处理

    最后在每次测试的时候都需要在Espresso注册这个IdlingResource

    @Before
    public void registerIdlingResource() {
        mIdlingResource = mActivityRule.getActivity().getIdlingResource();
        Espresso.registerIdlingResources(mIdlingResource);
    }
    
    @After
    public void unregisterIdlingResource() {
      if (mIdlingResource != null) {
        Espresso.unregisterIdlingResources(mIdlingResource);
      }
    }
    

    RecyclerView

    当要测试RecyclerView的时候需要添加如下依赖:

    // Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
    androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2'
    

    使用也比较简单,基本和一般view一样只是多了些方法

    onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.actionOnItemAtPosition(ITEM_BELOW_THE_FOLD, click()));
    
    onView(ViewMatchers.withId(R.id.recyclerView))
                    .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()));
    
    onView(withId(R.id.recycleviews)).perform(RecyclerViewActions.actionOnHolderItem(new    
        CustomViewHolderMatcher(hasDescendant(withText("Name"))), click()));
    

    最后说一下项目哪些需要测

    一般的逻辑与功能性代码使用JUnit+Mock

    • 所有的Model、Presenter/ViewModel、Api、Utils等类的public方法
    • Data类除了getter、setter、toString、hashCode等一般自动生成的方法之外的逻辑部分

    UI测试

    • 自定义View的功能:比如set data以后,text有没有显示出来等等,简单的交互,比如click事件,负责的交互一般不测,比如touch、滑动事件等等。
    • Activity的主要功能:比如view是不是存在、显示数据、错误信息、简单的点击事件等,组件之间intent交互。
    • 比较复杂的用户交互比如onTouch,以及view的样式、位置等等,一般直接人工测试。

    下面是我觉得比较适合学习

    Unit test with Mockito

    蘑菇街工程师写的单元测试教学合集

    官方自动化测试使用教学的例子

    官方推荐的App架构示例,可以找到和自己项目类似的,然后模仿其测试

    相关文章

      网友评论

        本文标题:Android单元测试的整理

        本文链接:https://www.haomeiwen.com/subject/qnwxqxtx.html