Android单元测试之Mockito

背景

在写单元测试的过程中,一个很普遍的问题是,要测试的目标类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。

Mock就是解决的方案。简单地说就是对测试的类所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。被测试对象像使用真的对象一样使用它们。用这种方式,我们可以把测试的目标限定于被测试对象本身,就如同在被测试对象周围做了一个划断,形成了一个尽量小的被测试目标。

Mockito是什么

Mockito是一套非常强大的测试框架,被广泛的应用于Java程序的unit test中。相比于EasyMock框架,Mockito使用起来简单,学习成本很低,而且具有非常简洁的API,测试代码的可读性很高。

Mockito使用

配置依赖:

testCompile "org.mockito:mockito-core:1.10.19"

先来看看Mockito的基础使用。比如我们有以下几个类:

public class Person {
    private int id;
    private String name;

    public Person(int id,String name){
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public interface PersonDAO {
    Person getPerson(int id);

    boolean update(Person person);
}
public class PersonService {
    private final PersonDAO personDAO;

    public PersonService(PersonDAO personDAO){
        this.personDAO = personDAO;
    }

    public boolean update(int id, String name) {
        Person person = personDAO.getPerson(id);
        if (person == null) {
            return false;
        }

        Person personUpdate = new Person(person.getId(), name);
        return personDAO.update(personUpdate);
    }
}

说明: 以上是开发中基础的mvc分层结构,比如在开发中,PersonDAO的具体实现还未完成,这时候就可以通过mock来mock一个实例来做测试。

来看一下测试时怎么写的,这里我们主要对PersonService 中的update方法写测试用例。

public class PersonServiceTest {

    private PersonDAO mockDao;
    private PersonService personService;

    @Before
    public void setUp() throws Exception {
        //模拟PersonDao对象
        mockDao = Mockito.mock(PersonDAO.class);
        Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,"Jim"));
        Mockito.when(mockDao.update(Mockito.isA(Person.class))).thenReturn(true);

        personService = new PersonService(mockDao);

    }

    @Test
    public void testUpdate() throws Exception {

        boolean result = personService.update(1,"Tom");
        assertTrue("is true",result);
        //验证是否执行过一次getPerson(1)
        Mockito.verify(mockDao,Mockito.times(1)).getPerson(Mockito.eq(1));
        //验证是否执行过一次update
        Mockito.verify(mockDao,Mockito.times(1)).update(Mockito.isA(Person.class));
    }

    @Test
    public void testUpdateNotFind() throws Exception {
        boolean result = personService.update(2, "new name");
        assertFalse("must true", result);
        //验证是否执行过一次getPerson(2)
        Mockito.verify(mockDao, Mockito.times(1)).getPerson(Mockito.eq(2));
        //验证是否执行过一次update
        Mockito.verify(mockDao, Mockito.never()).update(Mockito.isA(Person.class));
    }
}

简单说明一下:

  • 首先在setUp中,我们先模拟一个对象出来,主要通过Mockito.mock(PersonDAO.class);来mock的。
  • 然后添加Stubbind条件,Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,"Jim")); 意思是当调用mockDao.getPerson(1)时返回一个id为1,name为"Jim"的Person对象。
  • 在testUpdate()方法中 Mockito.verify(mockDao,Mockito.times(1)).getPerson(Mockito.eq(1));验证是否执行过一次getPerson(1)。只要有执行过Mockito都会记录下拉,所以这句是对的。

Mockito基础使用

Mockito的使用,有详细的api文档,具体可以查看:http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html, 下面是整理的一些常用的使用方式。

verify 验证

一旦创建,mock会记录所有交互,你可以验证所有你想要验证的东西,即使删掉也会有操作记录在。

@Test
public void testVerify() throws Exception {
    //mock creation
    List mockList = Mockito.mock(List.class);

    mockList.add("one");
    mockList.add("two");
    mockList.add("two");
    mockList.clear();

    //验证是否调用过一次 mockedList.add("one")方法,若不是(0次或者大于一次),测试将不通过,默认是一次
    Mockito.verify(mockList).add("one");
    //验证调用过2次 mockedList.add("two")方法,若不是,测试将不通过
    Mockito.verify(mockList,Mockito.times(2)).add("two");
    //验证是否调用过一次 mockedList.clear()方法,若没有(0次或者大于一次),测试将不通过
    Mockito.verify(mockList).clear();
}

这里主要注意。mock会记录你所有的操作的,即使删除也会记录下来。比如mockList中添加完,然后clear掉,Mockito.verify(mockList).add("one");这个的验证也是会通过的,验证的关键方法是verify, verify有两个重载方法:

  1. verify(T mock): 默认是验证调用一次,里面默认调用times(1)
  2. verify(T mock, VerificationMode mode):mode,调用次数.

Stubbing 条件

@Test
public void testStubbing() throws Exception{
    //你可以mock具体的类,而不仅仅是接口
    LinkedList mockedList = Mockito.mock(LinkedList.class);

    //设置值
    Mockito.when(mockedList.get(0)).thenReturn("one");
    Mockito.when(mockedList.get(1)).thenReturn("two");
    Mockito.when(mockedList.get(2)).thenReturn(new RuntimeException());

    //print 输出"one"
    System.out.println(mockedList.get(0));
    //输出 "java.lang.RuntimeException"
    System.out.println(mockedList.get(2));
    //这里会打印 "null" 因为 get(999) 没有设置
    System.out.println(mockedList.get(999));

    Mockito.verify(mockedList).get(0);
}
  1. 对于有返回值的方法,mock会默认返回null、空集合、默认值。比如,为int/Integer返回0,为boolean/Boolean返回false
  2. stubbing可以被覆盖,但是请注意覆盖已有的stubbing有可能不是很好
  3. 一旦stubbing,不管调用多少次,方法都会永远返回stubbing的值
  4. 当你对同一个方法进行多次stubbing,最后一次stubbing是最重要的

ArgumentMatcher参数匹配

@Test
public void testArgumentMatcher() throws Exception {
    LinkedList mockedList = Mockito.mock(LinkedList.class);
    //用内置的参数匹配器来stub
    Mockito.when(mockedList.get(Mockito.anyInt())).thenReturn("element");

    //打印 "element"
    System.out.println(mockedList.get(999));

    //你也可以用参数匹配器来验证,此处测试通过
    Mockito.verify(mockedList).get(Mockito.anyInt());

    //此处测试将不通过,因为没调用get(33)
    Mockito.verify(mockedList).get(Mockito.eq(33));
}

InvocationTimes验证准确的调用次数

验证准确的调用次数包括最多、最少、从未等,times(),never(),atLeast(),atMost().

/**
 * 验证准确的调用次数,最多、最少、从未等
 * @throws Exception
 */
@Test
public void testInvocationTimes() throws Exception {
    LinkedList mockedList = Mockito.mock(LinkedList.class);
    //using mock
    mockedList.add("once");

    mockedList.add("twice");
    mockedList.add("twice");

    mockedList.add("three times");
    mockedList.add("three times");
    mockedList.add("three times");

    //下面两个是等价的, 默认使用times(1)
    Mockito.verify(mockedList).add("once");
    Mockito.verify(mockedList, Mockito.times(1)).add("once");

    //验证准确的调用次数
    Mockito.verify(mockedList, Mockito.times(2)).add("twice");
    Mockito.verify(mockedList, Mockito.times(3)).add("three times");

    //从未调用过. never()是times(0)的别名
    Mockito.verify(mockedList, Mockito.never()).add("never happened");

    //用atLeast()/atMost()验证
    Mockito.verify(mockedList, Mockito.atLeastOnce()).add("three times");
    Mockito.verify(mockedList, Mockito.atLeast(2)).add("three times");

    //最多
    Mockito.verify(mockedList, Mockito.atMost(3)).add("three times");
}

为void方法抛异常

@Test
public void testVoidMethodsWithExceptions() throws Exception {
    LinkedList mockedList = Mockito.mock(LinkedList.class);
    Mockito.doThrow(new RuntimeException()).when(mockedList).clear();
    //这边会抛出异常
    mockedList.clear();
}

InOrder验证调用顺序

@Test
public void testVerificationInOrder() throws Exception {
    List singleMock = Mockito.mock(List.class);
    //使用单个mock对象
    singleMock.add("was added first");
    singleMock.add("was added second");

    //创建inOrder
    InOrder inOrder = Mockito.inOrder(singleMock);

    //验证调用次数,若是调换两句,将会出错,因为singleMock.add("was added first")是先调用的
    inOrder.verify(singleMock).add("was added first");
    inOrder.verify(singleMock).add("was added second");


    // 多个mock对象
    List firstMock = Mockito.mock(List.class);
    List secondMock = Mockito.mock(List.class);

    //using mocks
    firstMock.add("was called first");
    secondMock.add("was called second");

    //创建多个mock对象的inOrder
    inOrder = Mockito.inOrder(firstMock, secondMock);

    //验证firstMock先于secondMock调用
    inOrder.verify(firstMock).add("was called first");
    inOrder.verify(secondMock).add("was called second");
}

spy

spy是创建一个拷贝,如果你保留原始的list,并用它来进行操作,那么spy并不能检测到其交互

@Test
public void testSpy() throws Exception {
    List list = new LinkedList();
    List spy = Mockito.spy(list);

    //可选的,你可以stub某些方法
    Mockito.when(spy.size()).thenReturn(100);

    //如果操作原始list,那么spy是不会检测到的。
    list.add("first");

    //调用"真正"的方法
    spy.add("one");
    spy.add("two");

    //打印one
    System.out.println(spy.get(0));

    //size()方法被stub了,打印100
    System.out.println(spy.size());

    //可选,验证spy对象的行为
    Mockito.verify(spy).add("one");
    Mockito.verify(spy).add("two");

    //下面写法有问题,spy.get(10)会抛IndexOutOfBoundsException异常
    Mockito.when(spy.get(10)).thenReturn("foo");
    //可用以下方式
    Mockito.doReturn("foo").when(spy).get(10);
}

Captur 参数捕捉

@Test
public void testCapturingArguments() throws Exception {
    List mockedList = Mockito.mock(List.class);
    ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
    mockedList.add("John");

    //进行参数捕捉,这里参数应该是"John"
    Mockito.verify(mockedList).add(argument.capture());

    assertEquals("John",argument.getValue());
}

Mock 的 Annotation,

Mockito跟junit4一样也支持Annotation,Mockito支持的注解有:@Mock,@Spy(监视真实的对象),@Captor(参数捕获器),@InjectMocks(mock对象自动注入)。

Annotation的初始化

在使用Annotation注解之前,必须先初始化,一般初始化在Junit4的@Before里面,初始化的方法为:MockitoAnnotations.initMocks(testClass)参数testClass是你所写的测试类。

@Before
public void setUp() throws Exception {
    /**
     * 要想让Annotation起作用,就必须初始化.一般初始化都在@Before里面
     */
    MockitoAnnotations.initMocks(this);

}

@Mock注解

使用@Mock注解来定义mock对象有如下的优点:

  1. 方便mock对象的创建
  2. 减少mock对象创建的重复代码
  3. 提高测试代码可读性
  4. 变量名字作为mock对象的标示,所以易于排错

我们还是通过第一个例子来修改:

public class MockTest {
    @Mock
    private PersonDAO mockDao;
    private PersonService personService;
    
    @Before
    public void setUp() throws Exception {
        /**
         * 要想让Annotation起作用,就必须初始化.一般初始化都在@Before里面
         */
        MockitoAnnotations.initMocks(this);

        Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,"Jim"));
        Mockito.when(mockDao.update(Mockito.isA(Person.class))).thenReturn(true);
        personService = new PersonService(mockDao);

    }

    @Test
    public void testUpdate() throws Exception {

        boolean result = personService.update(1,"Tom");
        assertTrue("is true",result);
        //验证是否执行过一次getPerson(1)
        Mockito.verify(mockDao,Mockito.times(1)).getPerson(Mockito.eq(1));
        //验证是否执行过一次update
        Mockito.verify(mockDao,Mockito.times(1)).update(Mockito.isA(Person.class));
    }
}

结果和前面没用注解的一样。

@Spy注解

使用@Spy生成的类,所有方法都是真实方法,返回值和真实方法一样的,是使用Mockito.spy()的快捷方式.

public class MockTest {

    @Spy
    private List list = new LinkedList();
    
    @Before
    public void setUp() throws Exception {
        /**
         * 要想让Annotation起作用,就必须初始化.一般初始化都在@Before里面
         */
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void testSpy() throws Exception {
        //可选的,你可以stub某些方法
        Mockito.when(list.size()).thenReturn(100);


        //调用"真正"的方法
        list.add("one");
        list.add("two");

        //打印one
        System.out.println(list.get(0));

        //size()方法被stub了,打印100
        System.out.println(list.size());
    }

}

@Captor注解

@Captor是参数捕获器的注解,通过注解的方式可以更便捷的对ArgumentCaptor进行定义。还可以通过ArgumentCaptor对象的forClass(Class<T> clazz)方法来构建ArgumentCaptor对象,然后便可在验证时对方法的参数进行捕获,最后验证捕获的参数值。如果方法有多个参数都要捕获验证,那就需要创建多个ArgumentCaptor对象处理。

public class MockTest {

    @Captor
    private ArgumentCaptor<String>  captor;
    
    @Before
    public void setUp() throws Exception {
        /**
         * 要想让Annotation起作用,就必须初始化.一般初始化都在@Before里面
         */
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void testCaptor() throws Exception {
        /**
         * ArgumentCaptor的Api
         argument.capture() 捕获方法参数;
         argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值;
         argument.getAllValues() 方法进行多次调用后,返回多个参数值;

         */

        list.add("John");
        //进行参数捕捉,这里参数应该是"John"
        Mockito.verify(list).add(captor.capture());

        assertEquals("John",captor.getValue());

    }

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

推荐阅读更多精彩内容