Android单元测试之Mockito

作者: johnnycmj | 来源:发表于2017-09-01 09:44 被阅读159次

    背景

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

    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());
    
        }
    
    }
    

    相关文章

      网友评论

        本文标题:Android单元测试之Mockito

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