Android单元测试学习总结


Android单元测试主要分为以下两种

  • 本地单元测试(Junit Test), 本地单元测试是纯java代码的测试,只运行在本地电脑的JVM环境上,不依赖于Android框架的任何api, 因此执行速度快,效率较高,但是无法测试Android相关的代码。
  • 仪器化测试(Android Test),是针对Android相关代码的测试,需要运行在真机设备或模拟器上,运行速度较慢,但是可以测试UI的交互以及对设备信息的访问,得到接近真实的测试结果。

在Android Studio中新建一个项目的时候,appgradle中会默认添加单元测试的相关依赖库:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

其中testImplementation添加的依赖就是本地化测试库, androidTestImplementation 添加的依赖则是Android环境下的测试库,同时,在项目的工程目录下也会默认创建好测试的目录:

其中app/src/test/下面存放的是Junit本地测试代码,app/src/androidTest/下面存放的是Android测试代码。

一、本地单元测试

进行本地单元测试需要先了解一些基本的Junit注解:

注解名称含义
@Test定义所在方法为单元测试方法,方法必须是public void
@Before定义所在方法在每个测试用例执行之前执行一次, 用于准备测试环境(如: 初始化类,读输入流等),在一个测试类中,每个@Test方法的执行都会触发一次调用
@After定义所在方法在每个测试用例执行之后执行一次,用于清理测试环境数据,在一个测试类中,每个@Test方法的执行都会触发一次调用。
@BeforeClass定义所在方法在测试类里的所有用例运行之前运行一次,方法必须是public static void,用于做一些耗时的初始化工作(如: 连接数据库)
@AfterClass定义所在方法在测试类里的所有用例运行之后运行一次,方法必须是public static void,用于清理数据(如: 断开数据连接)
@Test (expected = Exception.class)如果该测试方法没有抛出Annotation中的Exception类型(子类也可以),则测试失败
@Test(timeout=100)如果该测试方法耗时超过100毫秒,则测试失败,用于性能测试
@Ignore 或者 @Ignore(“太耗时”)忽略当前测试方法,一般用于测试方法还没有准备好,或者太耗时之类的
@FixMethodOrder定义所在的测试类中的所有测试方法都按照固定的顺序执行,可以指定3个值,分别是DEFAULT、JVM、NAME_ASCENDING(字母顺序)
@RunWith指定测试类的测试运行器

更多可以参考Junit官网:https://junit.org/junit4/

1. 创建测试类

接下来就可以创建测试类,除了可以手动创建测试类外,可以利用AS快捷键:将光标选中要创建测试类的类名上->按下ALT + ENTER->在弹出的弹窗中选择Create Test

这会弹出下面的弹窗,或者鼠标在类名上右键选择菜单Go to–>Test,也会弹出下面的弹窗

勾选需要进行测试的方法,会自动生成一个测试类:

如果勾选了@Before@After的话也会自动给你生成对应的测试方法

接下来编写测试方法,首先在要测试的目标类中写几个业务方法:

public class SimpleClass {

    public boolean isTeenager(int age) {
        if (age < 15) {
            return true;
        }
        return false;
    }

    public int add(int a, int b) {
        return a + b;
    }

    public String getNameById(int id) {
        if (id == 1) {
            return "小明";
        } else if (id == 2){
            return "小红";
        }
        return "";
    }
}

然后,测试类:

@RunWith(JUnit4.class)
public class SimpleClassTest {
    private SimpleClass simpleClass;

    @Before
    public void setUp() throws Exception {
        simpleClass = new SimpleClass();
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void isTeenager() {
        Assert.assertFalse(simpleClass.isTeenager(20));
        Assert.assertTrue(simpleClass.isTeenager(14));
    }

    @Test
    public void add() {
        Assert.assertEquals(simpleClass.add(3, 2), 5);
        Assert.assertNotEquals(simpleClass.add(3, 2), 4);
    }

    @Test
    public void getNameById() {
        Assert.assertEquals(simpleClass.getNameById(1), "小明");
        Assert.assertEquals(simpleClass.getNameById(2), "小红");
        Assert.assertEquals(simpleClass.getNameById(10), "");
    }
}

其中setUp()是自动生成的添加了@Before注解,这会在每个测试方法执行前执行,因此在这里创建一个目标对象,也可以选择添加@BeforeClass注解但这时setUp()应该改为静态的方法。然后在每个测试方法中编写测试用例,这里使用org.junit.Assert包中的断言方法,有很多assertXXX方法,可以自己选择用来判断目标方法的结果是否满足预期。

2. Assert类中的常用断言方法

方法含义
assertNull(Object object)断言对象为空
assertNull(String message, Object object)断言对象为空,如果不为空抛出异常携带指定的message信息
assertNotNull(Object object)断言对象不为空
assertNotNull(Object object)断言对象不为空,如果为空抛出异常携带指定的message信息
assertSame(Object expected, Object actual)断言两个对象引用的是同一个对象
assertSame(String message, Object expected, Object actual)断言两个对象引用的是同一个对象,否则抛出异常携带指定的message信息
assertNotSame(Object expected, Object actual)断言两个对象引用的不是同一个对象
assertNotSame(String message, Object expected, Object actual)断言两个对象引用的不是同一个对象,否则抛出异常携带指定的message信息
assertTrue(boolean condition)断言结果为true
assertTrue(String message, boolean condition)断言结果为true, 为false时抛出异常携带指定的message信息
assertFalse(boolean condition)断言结果为false
assertFalse(String message, boolean condition)断言结果为false, 为true时抛出异常携带指定的message信息
assertEquals(long expected, long actual)断言两个long 类型 expected 和 actual 的值相等
assertEquals(String message, long expected, long actual)断言两个long 类型 expected 和 actual 的值相等,如不相等则抛异常携带指定message信息
assertEquals(Object expected, Object actual)断言两个对象相等
assertEquals(String message, Object expected, Object actual)断言两个对象相等,如果不相等则抛出异常携带指定的message信息
assertEquals(float expected, float actual, float delta)断言两个 float 类型 expect 和 actual 在 delta 偏差值下相等,delta是误差精度
assertEquals(String message, float expected, float actual, float delta)断言两个 float 类型 expect 和 actual 在 delta 偏差值下相等,如果不相等则抛出异常携带指定的message信息
assertEquals(double expected, double actual, double delta)断言两个 double 类型 expect 和 actual 在 delta 偏差值下相等
assertEquals(String message, double expected,double actual, double delta)断言两个 double 类型 expect 和 actual 在 delta 偏差值下相等,如果不相等则抛出异常携带指定的message信息
assertArrayEquals(T[] expected, T[] actual)断言两个相同类型的数组的元素一一对应相等
assertArrayEquals(String message, T[] expected, T[] actual)断言两个相同类型的数组的元素一一对应相等,如果不相等则抛出异常携带指定的message信息
fail()直接让测试失败
fail(String message)直接让测试失败并给出message错误信息
assertThat(T actual, Matcher<? super T> matcher)断言actual和matcher规则匹配
assertThat(String reason, T actual, Matcher<? super T> matcher)断言actual和matcher规则匹配,否则抛出异常携带指定的reason信息

其中assertEquals的方法,都对应有一个assertNotEquals方法,这里不列了,assertThat是一个强大的方法:

 Assert.assertThat(1, is(1));
 Assert.assertThat(0, is(not(1)));
 Assert.assertThat("hello", startsWith("h"));
 List<String> items = new ArrayList<>();
 items.add("aaa");
 items.add("bbb");
 Assert.assertThat(items, hasItem("aaa"));

需要静态导入org.hamcrest.Matchers类里面的方法,更多匹配方法请参考这个类。

3. 运行测试类

选中测试类右键Run运行,控制面板中就会显示测试结果:
在这里插入图片描述
如果所有的测试用例都正常返回了预期的结果,则面板中左侧每个测试方法前面会带一个绿色的对勾,否则方法前面会变成红色感叹号并且控制面板会输出异常,现在来改一个业务方法试一下:

    public boolean isTeenager(int age) {
        if (age < 15) {
            return false;
        }
        return false;
    }

这里将age < 15改为输出false,假设这是我们在编码的时候由于疏忽粗心造成的,然后运行测试类:
在这里插入图片描述
控制面板会告诉那一行出错了:
在这里插入图片描述
也就是说这里没有返回预期的结果,说明我们编写的业务逻辑是有错误的,这时就需要改bug了。

4. 运行单个测试方法或多个测试类

上面是运行的整个测试类,如果要运行测试类的单个方法,则鼠标只选中某个要运行的测试方法,然后右键选择Run即可。如果要同时运行多个测试类,而如果多个测试类在同一个包下面,则选中多个测试类所在的包目录,然后右键选择Run运行。否则可以通过下面的方式指定,创建一个空的测试类,然后添加注解:

@RunWith(Suite.class)
@Suite.SuiteClasses({SimpleClassTest.class, SimpleClass2Test.class})
public class RunMultiTest {
}

运行这个测试类就可以将指定的测试类的方法一起运行。

二、Mockito测试框架的使用

前面介绍的只能测试不涉及Android相关Api的java代码用例,如果涉及到Android相关Api的时候,就不方便了,这时如果不依赖第三方库的话可能需要使用仪器化测试跑到Android设备上去运行,于是有一些比较好的第三方的替代框架可以来模拟使用Android的代码测试,Mockito就是基于依赖注入实现的一个测试框架。

1. Mock概念的理解

什么是Mock, 这个单词的中文意思就是“模仿”或者“虚假”的意思,也就是要模仿一个对象,为啥要模仿?
在传统的JUnit单元测试中,没有消除在测试中对对象的依赖,如A对象依赖B对象方法,在测试A对象的时候,我们需要构造出B对象,这样子增加了测试的难度,或者使得我们对某些类的测试无法实现。这与单元测试的思路相违背。
还有一个主要的问题就是本地单元测试由于是运行本地JVM环境,无法依赖Android的api,只靠纯Junit的测试环境很难模拟出完整的Android环境,导致无法测试Android相关的代码,而Mock就能解决这个问题,通过Mock能够很轻易的实现对象的模拟。

添加依赖:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'org.mockito:mockito-core:2.19.0'
    ....
}

2. Mockito中几种Mock对象的方式

使用之前通过静态方式导入会使用更方便:

 // 静态导入会使代码更简洁
 import static org.mockito.Mockito.*;

直接mock一个对象:

    @Test
    public void testMock() {
        SimpleClass mockSimple = Mockito.mock(SimpleClass.class);
        assertNotNull(mockSimple);
    }

注解方式mock一个对象:

    @Mock
    SimpleClass simple;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testMock() {
        assertNotNull(simple);
    }

运行器方式mock一个对象:

@RunWith(MockitoJUnitRunner.class)
public class ExampleUnitTest {
    @Mock
    SimpleClass simple;
    
    @Test
    public void testMock() {
        assertNotNull(simple);
    }
}

MockitoRule方式mock一个对象:

public class ExampleUnitTest {
    @Mock
    SimpleClass simple;
    
    @Rule //<--使用@Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    
    @Test
    public void testMock() {
        assertNotNull(simple);
    }
}

3. 验证行为

verify(T mock)函数的使用

verify(T mock)的作用是验证发生的某些行为等同于verify(mock, times(1)) 例如:

@Test
public void testMock() {
	 //创建mock对象
	 List mockedList = mock(List.class);
	 //使用mock对象
	 mockedList.add("one");
	 mockedList.clear();
	
	 //验证mockedList.add("one")是否被调用,如果被调用则当前测试方法通过,否则失败
	 verify(mockedList).add("one");
	 //验证 mockedList.clear()是否被调用,如果被调用则当前测试方法通过,否则失败
	 verify(mockedList).clear();
 }
@Test
public void testMock() {
	mock.someMethod("some arg");
	//验证mock.someMethod("some arg")是否被调用,如果被调用则测试方法通过,否则失败
	verify(mock).someMethod("some arg");
	
}

也就是说如果把调用的方法注释掉,则运行testMock()方法就会失败。

通过verify关键字,一旦mock对象被创建了,mock对象会记住所有的交互。然后你就可能选择性的验证你感兴趣的交互。

通常需要配合一些测试方法来验证某些行为,这些方法称为"打桩方法"(Stub),打桩的意思是针对mock出来的对象进行一些模拟操作,如设置模拟的返回值或抛出异常等。

常见的打桩方法:

方法名方法含义
doReturn(Object toBeReturned)提前设置要返回的值
doThrow(Throwable… toBeThrown)提前设置要抛出的异常
doAnswer(Answer answer)提前对结果进行拦截
doCallRealMethod()调用某一个方法的真实实现
doNothing()设置void函数什么也不做
thenReturn(T value)设置要返回的值
thenThrow(Throwable… throwables)设置要抛出的异常
thenAnswer(Answer<?> answer)对结果进行拦截

例如:

 @Test
 public void testMock() {
	 // 你可以mock具体的类型,不仅只是接口
	 List mockedList = mock(List.class);
	 // 打测试桩
	 when(mockedList.get(0)).thenReturn("first");
	 doReturn("aaaa").when(mockedList).get(1);
	 when(mockedList.get(1)).thenThrow(new RuntimeException());
	 doThrow(new RuntimeException()).when(mockedList).clear();

	 // 输出“first”
	 System.out.println(mockedList.get(0));
	 // 因为get(999) 没有打桩,因此输出null, 注意模拟环境下这个地方是不会报IndexOutOfBoundsException异常的
	 System.out.println(mockedList.get(999));
	 // get(1)时会抛出异常
	 System.out.println(mockedList.get(1));
	 // clear会抛出异常
	 mockedList.clear();
 }

doXXXthenXXX使用上差不多,一个是调用方法之前设置好返回值,一个是在调用方法之后设置返回值。默认情况下,Mock出的对象的所有非void函数都有返回值,对象类型的默认返回的是null,例如返回int、boolean、String的函数,默认返回值分别是0、falsenull

使用when(T methodCall)函数

打桩方法需要配合when(T methodCall)函数,意思是使测试桩方法生效。当你想让这个mock能调用特定的方法返回特定的值,那么你就可以使用它。

例如:

when(mock.someMethod()).thenReturn(10);
 //你可以使用灵活的参数匹配,例如 
 when(mock.someMethod(anyString())).thenReturn(10);

 //设置抛出的异常
 when(mock.someMethod("some arg")).thenThrow(new RuntimeException());

 //你可以对不同作用的连续回调的方法打测试桩:
 //最后面的测试桩(例如:返回一个对象:"foo")决定了接下来的回调方法以及它的行为。
 
 when(mock.someMethod("some arg"))
  .thenReturn("foo")//第一次调用someMethod("some arg")会返回"foo"
  .thenThrow(new RuntimeException());//第二次调用someMethod("some arg")会抛异常
  
 //可以用以下方式替代比较小版本的连贯测试桩:
 when(mock.someMethod("some arg"))
  .thenReturn("one", "two");
 //和下面的方式效果是一样的
 when(mock.someMethod("some arg"))
  .thenReturn("one")
  .thenReturn("two");

 //比较小版本的连贯测试桩并且抛出异常:
 when(mock.someMethod("some arg"))
  .thenThrow(new RuntimeException(), new NullPointerException();
使用thenAnswer为回调做测试桩
when(mock.someMethod(anyString())).thenAnswer(new Answer() {
     Object answer(InvocationOnMock invocation) {
         Object[] args = invocation.getArguments();
         Object mock = invocation.getMock();
         return "called with arguments: " + args;
     }
 });

 // 输出 : "called with arguments: foo"
 System.out.println(mock.someMethod("foo"));
使用doCallRealMethod()函数来调用某个方法的真实实现方法

注意,在Mock环境下,所有的对象都是模拟出来的,而方法的结果也是需要模拟出来的,如果你没有为mock出的对象设置模拟结果,则会返回默认值,例如:

public class Person {
    public String getName() {
        return "小明";
    }
}

@Test
public void testPerson() {
    Person mock = mock(Person.class);
    //输出null,除非设置发回模拟值when(mock.getName()).thenReturn("xxx");
    System.out.println(mock.getName());
}

因为getName()方法没有设置模拟返回值,而getName()返回值是String类型的,因此直接调用的话会返回String的默认值null,所以上面代码如果要想输出getName()方法的真实返回值的话,需要设置doCallRealMethod():

 @Test
 public void testPerson() {
     Person mock = mock(Person.class);
     doCallRealMethod().when(mock).getName();
     //输出“小明”
     System.out.println(mock.getName());
 }
使用doNothing()函数是为了设置void函数什么也不做

需要注意的是默认情况下返回值为void的函数在mocks中是什么也不做的但是,也会有一些特殊情况。如:

测试桩连续调用一个void函数时:

   doNothing().doThrow(new RuntimeException()).when(mock).someVoidMethod();
   //does nothing the first time:
   mock.someVoidMethod();
   //throws RuntimeException the next time:
   mock.someVoidMethod();

监控真实的对象并且你想让void函数什么也不做:

List list = new LinkedList();
List spy = spy(list);

//let's make clear() do nothing
doNothing().when(spy).clear();

spy.add("one");

//clear() does nothing, so the list still contains "one"
spy.clear();
使用doAnswer()函数测试void函数的回调

当你想要测试一个无返回值的函数时,可以使用一个含有泛型类Answer参数的doAnswer()函数做回调测试。假设你有一个void方法有多个回调参数,当你想指定执行某个回调时,使用thenAnswer很难实现了,如果使用doAnswer()将非常简单,示例代码如下:

MyCallback callback = mock(MyCallback.class);
Mockito.doAnswer(new Answer() {
    @Override
    public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
        //获取第一个参数
        MyCallback call = invocation.getArgument(0);
        //指定回调执行操作
        call.onSuccess();
        return null;
    }

}).when(mockedObject.requset(callback));

doAnswer(new Answer() {
         @Override
          public Object answer(InvocationOnMock invocation) throws Throwable {
              System.out.println("onSuccess answer");
              return null;
          }
 }).when(callback).onSuccess();
 
mockedObject.requset(callback)
需要使用doReturn函数代替thenReturn的情况

如当监控真实的对象并且调用真实的函数带来的影响时

List list = new LinkedList();
List spy = spy(list);

//不可能完成的:真实方法被调用的时候list仍是空的,所以spy.get(0)会抛出IndexOutOfBoundsException()异常
when(spy.get(0)).thenReturn("foo");

//这时你应该使用doReturn()函数
doReturn("foo").when(spy).get(0);
使用doThrow()函数来测试void函数抛出异常
SimpleClass mock = mock(SimpleClass.class);
doThrow(new RuntimeException()).when(mock).someVoidMethod();
mock.someVoidMethod();

总之使用doThrow(), doAnswer(), doNothing(), doReturn() and doCallRealMethod() 这些函数时可以在适当的情况下调用when()来解决一些问题., 如当你需要下面这些功能时这是必须的:

  • 测试void函数
  • 在受监控的对象上测试函数
  • 不只一次的测试同一个函数,在测试过程中改变mock对象的行为

4. 验证方法的调用次数

需要配合使用一些方法

方法含义
times(int wantedNumberOfInvocations)验证调用方法的次数
never()验证交互没有发生,相当于times(0)
only()验证方法只被调用一次,相当于times(1)
atLeast(int minNumberOfInvocations)至少进行n次验证
atMost(int maxNumberOfInvocations)至多进行n次验证
after(long millis)在给定的时间后进行验证
timeout(long millis)验证方法执行是否超时
description(String description)验证失败时输出的内容
verifyZeroInteractions验证mock对象没有交互

例如:

mock.someMethod("some arg");
mock.someMethod("some arg");
//验证mock.someMethod("some arg")被连续调用两次,即如果没有调用两次则验证失败
verify(mock, times(2)).someMethod("some arg");
//注意,下面三种是等价的,都是验证someMethod()被只调用一次
verify(mock).someMethod("some arg");
verify(mock, times(1)).someMethod("some arg");
verify(mock, only()).someMethod("some arg");
mPerson.getAge();
mPerson.getAge();
//验证至少调用2次
verify(mPerson, atLeast(2)).getAge();
//验证至多调用2次
verify(mPerson, atMost(2)).getAge();
//下面两种等价,验证调用次数为0
verify(mPerson, never()).getAge();
verify(mPerson, times(0)).getAge();
mPerson.getAge();
mPerson.getAge();
long current = System.currentTimeMillis();
System.out.println(current );
//延时1s后验证mPerson.getAge()是否被执行了2次
verify(mPerson, after(1000).times(2)).getAge();
System.out.println(System.currentTimeMillis() - current);
 mPerson.getAge();
 mPerson.getAge();
 //验证方法在100ms超时前被调用2次
 verify(mPerson, timeout(100).times(2)).getAge();
  @Test
  public void testVerifyZeroInteractions() {
      Person person = mock(Person.class);
      person.eat("a");
	  //由于person对象发生了交互,所以这里验证失败,把上面的调用注释掉这里就会验证成功
      verifyZeroInteractions(person);
      //可以验证多个对象没有交互
      //verifyZeroInteractions(person,person2 );
  }
  @Test
  public void testVerifyZeroInteractions() {
      Person person = mock(Person.class);
      person.eat("a");
      verify(person).eat("a");
	  //注意,这将会无法到达验证目的,不能跟verify()混用
      verifyZeroInteractions(person,person2 );
  }

5. 参数匹配器 (matchers)

Mockito以自然的java风格来验证参数值: 使用equals()函数。有时,当需要额外的灵活性时你可能需要使用参数匹配器,也就是argument matchers :

// 使用内置的anyInt()参数匹配器
 when(mockedList.get(anyInt())).thenReturn("element");

 // 使用自定义的参数匹配器( 在isValid()函数中返回你自己的匹配器实现 )
 when(mockedList.contains(argThat(isValid()))).thenReturn("element");

 // 输出element
 System.out.println(mockedList.get(999));

 // 你也可以验证参数匹配器
 verify(mockedList).get(anyInt());

常用的参数匹配器:

方法名含义
anyObject()匹配任何对象
any(Class type)与anyObject()一样
any()与anyObject()一样
anyBoolean()匹配任何boolean和非空Boolean
anyByte()匹配任何byte和非空Byte
anyCollection()匹配任何非空Collection
anyDouble()匹配任何double和非空Double
anyFloat()匹配任何float和非空Float
anyInt()匹配任何int和非空Integer
anyList()匹配任何非空List
anyLong()匹配任何long和非空Long
anyMap()匹配任何非空Map
anyString()匹配任何非空String
contains(String substring)参数包含给定的substring字符串
argThat(ArgumentMatcher matcher)创建自定义的参数匹配模式
eq(T value)匹配参数等于某个值

一些示例代码:

    @Test
    public void testPersonAny(){
        when(mPerson.eat(any(String.class))).thenReturn("米饭");
        //或:
        when(mPerson.eat(anyString())).thenReturn("米饭");
        //输出米饭
        System.out.println(mPerson.eat("面条"));
    }

    @Test
    public void testPersonContains(){
        when(mPerson.eat(contains("面"))).thenReturn("面条");
        //输出面条
        System.out.println(mPerson.eat("面"));
    }

    @Test
    public void testPersonArgThat(){
        //自定义输入字符长度为偶数时,输出面条。
        when(mPerson.eat(argThat(new ArgumentMatcher<String>() {
            @Override
            public boolean matches(String argument) {
                return argument.length() % 2 == 0;
            }
        }))).thenReturn("面条");
        //输出面条
        System.out.println(mPerson.eat("1234"));
    }

需要注意的是,如果你打算使用参数匹配器,那么所有参数都必须由匹配器提供。例如:

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// 上述代码是正确的,因为eq()也是一个参数匹配器

verify(mock).someMethod(anyInt(), anyString(), "third argument");
// 上述代码是错误的, 因为所有参数必须由匹配器提供,而参数"third argument"并非由参数匹配器提供,因此会抛出异常

像anyObject(), eq()这样的匹配器函数不会返回匹配器。它们会在内部将匹配器记录到一个栈当中,并且返回一个假的值,通常为null。

6. 使用InOrder验证执行执行顺序

验证执行执行顺序主要使用InOrder函数
如,验证mock一个对象的函数执行顺序:

    @Test
    public void testInorder() {
        List<String> singleMock = mock(List.class);

        singleMock.add("小明");
        singleMock.add("小红");

        // 为该mock对象创建一个inOrder对象
        InOrder inOrder = inOrder(singleMock);

        // 验证add函数首先执行的是add("小明"),然后才是add("小红"),否则测试失败
        inOrder.verify(singleMock).add("小明");
        inOrder.verify(singleMock).add("小红");
    }

验证多个mock对象的函数执行顺序:

    @Test
    public void testInorderMulti() {
        List<String> firstMock = mock(List.class);
        List<String> secondMock = mock(List.class);

        firstMock.add("小明");
        secondMock.add("小红");

        // 为这两个Mock对象创建inOrder对象
        InOrder inOrder = inOrder(firstMock, secondMock);

        // 验证它们的执行顺序
        inOrder.verify(firstMock).add("小明");
        inOrder.verify(secondMock).add("小红");
    }

验证执行顺序是非常灵活的,你不需要一个一个的验证所有交互,只需要验证你感兴趣的对象即可。 你可以选择单个mock对象和多个mock对象混合着来,也可以仅通过那些需要验证顺序的mock对象来创建InOrder对象。

7. 使用Spy监控真实对象

监控真实对象使用spy()函数生成,或者也可以像@Mock那样使用@Spy注解来生成一个监控对象, 当你你为真实对象创建一个监控(spy)对象后,在你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了。尽量少使用spy对象,使用时也需要小心形式。

    @Test
    public void testSpy() {
        List<String> list = new ArrayList<>();
        List<String> spy = spy(list);

        // 你可以选择为某些函数打桩
        when(spy.size()).thenReturn(100);

        // 调用真实对象的函数
        spy.add("one");
        spy.add("two");

        // 输出第一个元素"one"
        System.out.println(spy.get(0));

        // 因为size()函数被打桩了,因此这里返回的是100
        System.out.println(spy.size());

        // 验证交互
        verify(spy).add("one");
        verify(spy).add("two");
    }

使用@Spy生成监控对象:

    @Spy
    Person mSpyPerson;

    @Test
    public void testSpyPerson() {
    	//将会输出Person 类中getName()的真实实现,而不是null
        System.out.println(mSpyPerson.getName());
    }

理解监控真实对象非常重要!有时,在监控对象上使用when(Object)来进行打桩是不可能或者不切实际的。因此,当使用监控对象时请考虑doReturn|Answer|Throw()函数族来进行打桩。例如:

List list = new LinkedList();
List spy = spy(list);

// 不可能实现 : 因为当调用spy.get(0)时会调用真实对象的get(0)函数,
// 此时会发生IndexOutOfBoundsException异常,因为真实List对象是空的
 when(spy.get(0)).thenReturn("foo");

// 你需要使用doReturn()来打桩
doReturn("foo").when(spy).get(0);

8. 使用ArgumentCaptor进行参数捕获

参数捕获主要为了下一步的断言做准备,示例代码:

    @Test
    public void argumentCaptorTest() {
        List<Object> mock = mock(List.class);
        mock.add("John");
		 //构建要捕获的参数类型,这里是String
        ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
        //在verify方法的参数中调用argument.capture()方法来捕获输入的参数
        verify(mock).add(argument.capture());
        //验证“John”参数捕获
        assertEquals("John", argument.getValue());
    }
    @Test
    public void argumentCaptorTest2() {
        List<Object> mock = mock(List.class);
        mock.add("Brian");
        mock.add("Jim");

        ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
        verify(mock, times(2)).add(argument.capture());
        //如果又多次参数调用,argument.getValue()捕获到的是最后一次调用的参数
        assertEquals("Jim", argument.getValue());
        //如果要获取所有的参数值可以调用argument.getAllValues()
        assertArrayEquals(new Object[]{"Brian","Jim"}, argument.getAllValues().toArray());
    }

9. 使用@InjectMocks自动注入依赖对象

有时我们要测试的对象内部需要依赖另一个对象,例如:

public class User {
    private Address address;

    public void setAddress(Address address) {
        this.address = address;
    }

    public String getAddress() {
        return address.getDetail();
    }
}
public class Address {
    public String getDetail() {
        return "detail Address";
    }
}

User类内部需要依赖Address类,当我们测试的时候需要mock出这两个对象,然后将Address对象传入到User当中,这样如果依赖的对象多了的话就相当麻烦,Mockito 提供了可以不用去手动注入对象的方法,首先使用@InjectMocks注解需要被注入的对象,如User,然后需要被依赖注入的对象使用@Mock@Spy注解,之后Mockito 会自动完成注入过程,例如:

    @InjectMocks
    User mTestUser;
    @Mock
    Address mAddress;
    @Test
    public void argumentInjectMock() {
        when(mAddress.getDetail()).thenReturn("浙江杭州");
        System.out.println(mTestUser.getAddress());
    }

这样就不用关心为User 设置Address ,只要为User需要依赖的类添加注解就可以了,然后直接将重点放到测试方法的编写上。

或者使用@Spy监控真实对象注入也可以:

    @InjectMocks
    User mTestUser;
    @Spy
    Address mAddress;
    @Test
    public void argumentInjectMock() {
        //  when(mAddress.getDetail()).thenReturn("浙江杭州");
        System.out.println(mTestUser.getAddress());
    }

其他:

连续调用的另一种更简短的版本:

// 第一次调用时返回"one",第二次返回"two",第三次返回"three"
 when(mock.someMethod("some arg")).thenReturn("one", "two", "three");

参考:Mockito 中文文档

三、PowerMockito框架使用

Mockito框架基本满足需求但是有一些局限性,如对static、final、private等方法不能mock,PowerMockito就可以解决这些问题,PowerMockito是一个扩展了其它如EasyMock等mock框架的、功能更加强大的框架。PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。

添加依赖:

    testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
    testImplementation 'org.powermock:powermock-module-junit4-rule:2.0.2'
    testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
    testImplementation 'org.powermock:powermock-classloading-xstream:2.0.2'

1. 普通Mock的方式

目标类:

public class CommonExample {
    public boolean callArgumentInstance(File file) {
        return file.exists();
    }
}

测试类:

public class CommonExamplePowerMockTest {
    @Test
    public void testCallArgumentInstance() {
        File file = PowerMockito.mock(File.class);
        CommonExample commonExample = new CommonExample();
        PowerMockito.when(file.exists()).thenReturn(true);
        Assert.assertTrue(commonExample.callArgumentInstance(file));
    }
 }

普通Mock方式是外部传递Mock参数,基本上和单独使用Mockito是一样的,使用纯Mockito的api也可以完成这个测试。

2. Mock方法内部new出来的对象

public class CommonExample {
    public boolean callArgumentInstance(String path) {
        File file = new File(path);
        return file.exists();
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest(CommonExample.class)
public class CommonExamplePowerMockTest {
    @Test
    public void callCallArgumentInstance2() throws Exception {
        File file = PowerMockito.mock(File.class);
        CommonExample commonExample = new CommonExample();
        PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file);
        PowerMockito.when(file.exists()).thenReturn(true);
        Assert.assertTrue(commonExample.callArgumentInstance("aaa"));
    }
}

跟前面有一点区别的就是,这里要测试的方法内部创建了File对象,这时需要通过PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file)方法模拟创建File的操作,当File类以aaa的参数创建的时候返回已经mock出来的file对象。同时这时需要在测试类上添加注解@RunWith(PowerMockRunner.class)@PrepareForTest(CommonExample.class),注意是在类上面添加,不是在方法上,一开始在方法上添加时提示找不到测试方法,@PrepareForTest()括号里面指定的是要测试的目标类。

3. Mock普通对象的final方法

public class CommonExample {
    public boolean callFinalMethod(DependencyClass dependency) {
        return dependency.isValidate();
    }
}

public class DependencyClass {
    public final boolean isValidate() {
        // do something
        return false;
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({CommonExample.class, DependencyClass.class})
public class CommonExamplePowerMockTest {
    @Test
    public void callFinalMethod() {
        DependencyClass dependency = PowerMockito.mock(DependencyClass.class);
        CommonExample commonExample = new CommonExample();
        PowerMockito.when(dependency.isValidate()).thenReturn(true);
        Assert.assertTrue(commonExample.callFinalMethod(dependency));
    }
}

同样这里mock出来需要依赖的类的对象,然后传递给调用方法,这里同样需要添加@RunWith@PrepareForTest@PrepareForTest可以指定多个目标类,但是这里如果你只需要测试final的话,只添加DependencyClass.class一个就可以了。

4. Mock普通类的静态方法

public final class Utils {
    public static String getUUId() {
        return UUID.randomUUID().toString();
    }
}

public class CommonExample {
    public String printUUID() {
        return Utils.getUUId();
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest(Utils.class)
public class StaticUnitTest {

    @Before
    public void setUp() throws Exception {
        PowerMockito.mockStatic(Utils.class);
    }

    @Test
    public void getUUId() {
        PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
        CommonExample commonExample = new CommonExample();
        assertThat(commonExample.printUUID(), is("FAKE UUID"));
    }
}

同样需要指定@RunWith@PrepareForTest@PrepareForTest中指定静态方法所在的类,测试静态方法之前需要调用PowerMockito.mockStatic()方法来mock静态类,然后就通过when().thenReturn()方法指定静态方法的模拟返回值即可。

5. verify静态方法的调用次数

@Test
    public void testVerify() {
        PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
        CommonExample commonExample = new CommonExample();
        System.out.println(commonExample.printUUID());
        PowerMockito.verifyStatic(Utils.class);
        Utils.getUUId();
    }

静态方法通过PowerMockito.verifyStatic(Class c)进行验证,不过这里跟Mocktio有一点区别的是需要在这个方法的后面再调用一次静态方法,否则不行。这里PowerMockito.verifyStatic(Utils.class)其实等同于PowerMockito.verifyStatic(Utils.class, times(1)),如果想验证超过一次的,那么:

    @Test
    public void testVerify() {
        PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
        CommonExample commonExample = new CommonExample();
        System.out.println(commonExample.printUUID());
        System.out.println(commonExample.printUUID());
        PowerMockito.verifyStatic(Utils.class, Mockito.times(2));
        Utils.getUUId();
    }

这时PowerMockito.verifyStatic()第一个参数指定静态方法类的Class,第二个参数接收一个VerificationMode类型的参数,因此传递Mockito中的任何验证方法次数的函数都可以,Mockito中的验证函数会返回的是一个VerificationMode类型。同样在PowerMockito.verifyStatic方法后面要调用一次要验证的静态方法,总感觉这里很奇怪。。。

6. 使用真实返回值

如果在测试的过程中又遇到不需要mock出来的静态方法的模拟返回值,而是需要真实的返回值,怎么办呢,其实跟Mockito一样,PowerMockito同样提供thenCallRealMethod或者doCallRealMethod方法:

    @Test
    public void testRealCall() throws Exception {
        PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
        //...
        PowerMockito.when(Utils.getUUId()).thenCallRealMethod();
        //与下面等价
        //PowerMockito.doCallRealMethod().when(Utils.class, "getUUId");
        System.out.println(Utils.getUUId());
    }

或者直接使用spy监控真实对象也可以:

    @Test
    public void testRealCall() {
        PowerMockito.spy(Utils.class);
        System.out.println(Utils.getUUId());
    }

7. Mock私有方法

public class CommonExample {
    public boolean callPrivateMethod() {
        return isExist();
    }
    private boolean isExist() {
        return false;
    }
 }
@RunWith(PowerMockRunner.class)
@PrepareForTest(CommonExample.class)
public class PrivateUnitTest {
    @Test
    public void testCallPrivateMethod() throws Exception {
        CommonExample commonExample = PowerMockito.mock(CommonExample.class);
        PowerMockito.when(commonExample.callPrivateMethod()).thenCallRealMethod();
        PowerMockito.when(commonExample, "isExist").thenReturn(true);
        Assert.assertTrue(commonExample.callPrivateMethod());
    }
}

在使用上跟纯Mockito的没有太大区别,只不过Mock私有方法是通过下面的api实现的:

PowerMockito.when(Object instance, String methodName, Object... arguments)

在PowerMockito中when函数与Mockito相比,最大的变化就是多了一些传递String类型的methodName的重载方法,这样在使用上几乎无所不能了。

8. Mock普通类的私有变量

public class CommonExample {
    private static final int STATE_NOT_READY = 0;
    private static final int STATE_READY = 1;
    private int mState = STATE_NOT_READY;
    
    public boolean doSomethingIfStateReady() {
        if (mState == STATE_READY) {
            // DO some thing
            return true;
        } else {
            return false;
        }
    }
 }
    @Test
    public void testDoSomethingIfStateReady() throws Exception {
        CommonExample sample = new CommonExample();
        Whitebox.setInternalState(sample, "mState", 1);
        assertThat(sample.doSomethingIfStateReady(), is(true));
    }

通过Whitebox.setInternalState来改变私有成员变量,这种情况下不需要指定@RunWith@PrepareForTest

9. 对静态void方法进行Mock

public class CommonExample {
    public static void doSomething(String a) {
        System.out.println("doSomething"+a);
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({CommonExample.class})
public class StaticUnitTest {
   @Test
    public void testStaticVoid() throws Exception {
        PowerMockito.mockStatic(CommonExample.class);
        PowerMockito.doNothing().when(CommonExample.class, "doSomething", Mockito.any());
        CommonExample.doSomething("aaa");
    }
}

默认情况下通过PowerMockito.mockStatic的静态类的void的方法是什么也不做的,但是可以显示的执行doNothing, 上面的代码将doNothing那行注释掉也是什么也不做的。那如果想做一些事而不是doNothing呢,跟Mockito一样,采用doAnswer:

    @Test
    public void testStaticVoid() throws Exception {
        PowerMockito.mockStatic(CommonExample.class);
        PowerMockito.doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                System.out.println(invocation.getArguments()[0]);
                return null;
            }
        }).when(CommonExample.class, "doSomething", Mockito.any());
        CommonExample.doSomething("aaa");
    }

10. Mock系统的final静态类

public class CommonExample {
    public int callSystemStaticMethod(int a, int b) {
        return Math.max(a, a+b);
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest(CommonExample.class)
public class StaticUnitTest {
	@Test
    public void callSystemStaticMethod() {
        CommonExample commonExample = new CommonExample();
        PowerMockito.mockStatic(Math.class);
        PowerMockito.when(Math.max(anyInt(), anyInt())).thenReturn(100);
        Assert.assertEquals(100, commonExample.callSystemStaticMethod(10, -5));
    }
}

@PrepareForTest中添加调用系统类所在的类,这里需要注意的是如果你使用PowerMockito来mock系统静态final类,则gradle依赖中不能再添加单纯Mockito的依赖库,否则这里将不能mock成功,会提示Mockito can not mock/spy final class, 因为PowerMockito本身已经有对Mockito的依赖库支持了,所以只依赖PowerMockito就可以了。除了系统静态final类的情况,其他的情况下PowerMockito和Mockito可以同时依赖(我测试是没有问题的)。另外单纯的Mockito新版本中也支持对 final 类 final 方法的 Mock,但是需要添加配置文件并不友好。

四、Robolectric测试框架的使用

由于Robolectric部分的内容比较长,所以单独放了一篇文章中:Android单元测试框架Robolectric的学习使用

五、Espresso测试框架的使用

Espresso是用于Android仪器化测试的测试框架,是谷歌官方主推的一个测试库。由于Espresso部分的内容也比较长,所以单独放了一篇文章中:Espresso测试框架的使用


版权声明:本文为lyabc123456原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。