Android单元测试的整理

也是以前就整理,最近在强化练习,所以顺便系统整理了一下,测试的话,随着项目便的复杂个人觉得越来越重要,还是偏向于使用,我也不深究原理了。
最近实践,个人比较喜欢采用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架构示例,可以找到和自己项目类似的,然后模仿其测试

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容