美文网首页Java
单元测试 Mockito

单元测试 Mockito

作者: 虐心笔记 | 来源:发表于2020-11-22 14:29 被阅读0次

    一、前言

    之前接触到一次SaaS项目,在进行Dubbo接口测试时,由于某些API是回调接口通过MQ交互传递消息的,走的异步处理。因为MQ在消费消息的时候可能存在延迟,在自动化用例调用接口步骤之后,立即查询数据库结果可能会存在消息还在消费中数据并没有落库,导致断言case会失败,而接口本身定义返回的 message 对于assert没有实际的意义,这就造成了在assert时候的麻烦。因为你不知道MQ消息何时才消费完成并落库,导致用例的不确定性影响通过率。刚开始的思路是通过多次遍历查询数据库,但是结果并不是很理想,因为遍历等待的时间会严重影响用例执行的效率,通过率也不符合预期。

    针对以上问题,经过考虑是否能够采用mock的方式来解决。上网查询相关资料,发现 Mockito+PowerMockito 框架,提供的相关API很好的解决了以上问题,不得不说 Mockito 是真的好用,强烈推荐真香!


    二、Mockito 入门

    POM依赖
      <dependencies>
        <!-- https://mvnrepository.com/artifact/org.testng/testng -->
        <dependency>
          <groupId>org.testng</groupId>
          <artifactId>testng</artifactId>
          <version>7.1.0</version>
          <scope>test</scope>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.powermock/powermock-api-mockito2 -->
        <dependency>
          <groupId>org.powermock</groupId>
          <artifactId>powermock-api-mockito2</artifactId>
          <version>2.0.9</version>
        </dependency>
    
      </dependencies>
    
    模拟对象
            // 1.Mock Person Object
            Person person = mock(Person.class);
            // 2.此时调用getName方法,会返回null,因为还没有对方法调用的返回值做Mock
            System.out.println(person.getName());
    
    模拟对象返回值
            // 1.Mock Person Object
            Person person = mock(Person.class);
            // 2.Mock 调用person对象的getName()方法时,返回angst,在给特定的方法调用返回固定值在官方说法中称为 stub 测试桩
            when(person.getName()).thenReturn("angst");
            // 3.此时打印输出 angst
            System.out.println(person.getName());
    
    模拟指定异常
            // 1.Mock Person Object
            Person person = mock(Person.class);
            // 2.模拟调用getAge()方法时,抛出 RuntimeException
            when(person.getAge()).thenThrow(new RuntimeException());
            // 3.此时将会抛出 RuntimeException
            System.out.println(person.getAge());
    
    为返回值为void的函数通过Stub抛出异常
            // 1.Mock Person Object
            Person person = mock(Person.class);
            // 2.用于为无返回值的函数打桩
            doThrow(new RuntimeException("Custom Exception")).when(person).setAge(18);
            // 3.调用这句代码会抛出异常
            person.setAge(18);
    
    参数匹配器 (matchers)
            // 1.Mock Person Object
            LinkedList mockedList = mock(LinkedList.class);
            // 使用内置的anyInt()参数匹配器,anyInt代表任意int
            when(mockedList.get(anyInt())).thenReturn("anyInt");
            //following prints "anyInt"
            System.out.println(mockedList.get(999));
            //you can also verify using an argument matcher
            verify(mockedList).get(anyInt());
    
    验证调用次数
            // 1.Mock Person Object
            Person person = mock(Person.class);
            person.setName("amy");
            person.setName("angst");person.setName("angst");
            person.setName("thin bamboo");person.setName("thin bamboo");person.setName("thin bamboo");
            /* 2.下面两个写法验证效果一样,均验证 setName() 方法是否被调用了一次,verify 函数默认验证的是执行了times(1)
            也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了,如果 times=2 则抛出异常 TooLittleActualInvocations*/
            verify(person).setName("amy");
            verify(person, times(1)).setName("amy");
            // 3.使用never()进行验证,never相当于times(0)
            verify(person, never()).setAge(18);
            //atLeastOnce相当于times(1)
            verify(person, atLeastOnce()).setName("amy");
            //atLeast至少调用N次
            verify(person, atLeast(2)).setName("angst");
            //atLeast最多调用N次
            verify(person, atMost(3)).setName("thin bamboo");
    
    验证执行执行顺序
            // 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 inOrder2 = inOrder(firstMock, secondMock);
            //following will make sure that firstMock was called before secondMock
            inOrder2.verify(firstMock).add("was called first");
            inOrder2.verify(secondMock).add("was called second");
            // Oh, and A + B can be mixed together at will
    
    为连续的调用做测试桩 (stub)
            // 1.Mock Person Object
            Person person = mock(Person.class);
            // 2.第一次调用返回 "angst",第二次返回"amy",第三次返回"Exception"
            when(person.getName()).thenReturn("angst").thenReturn("amy").thenThrow(new RuntimeException("第三次调用会返回异常"));
            System.out.println("第一次调用:" + person.getName());
            System.out.println("第二次调用:" + person.getName());
            System.out.println("第三次调用:" + person.getName());
            // 3.另外,连续调用的另一种更简短的版本
            when(person.getName()).thenReturn("angst", "amy").thenThrow(new RuntimeException("第三次调用会返回异常"));
    
    为回调做测试桩
            // 1.Mock Person Object
            Person person = mock(Person.class);
            // 2.运行为泛型接口 Answer 打桩
            when(person.getName()).thenAnswer(new Answer() {
                public Object answer(InvocationOnMock invocation) {
                    Object[] args = invocation.getArguments();
                    Object mock = invocation.getMock();
                    return "called with arguments: " + Arrays.toString(args) + " object: "+ mock;
                }
            });
            //Following prints "called with arguments: [] object: Mock for Person, hashCode: 426394307"
            System.out.println(person.getName());
    
    简化mock对象的创建
        @Mock
        private Person person;
        @Mock
        private UserSearchServiceImpl userSearchService;
    
        //注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中
        @BeforeClass
        public void setUp(){ MockitoAnnotations.initMocks(this); }
    

    三、模拟真实接口实战

    1.正常模拟场景

    首先来 mock 一个正常接口方法的返回值。假设需要测试一个 UserSearchServiceImpl#getInfo 接口方法,需要对接口返回修改指定的返回值,然后调用该接口,最后断言接口返回。下面看案例:PersonTest.java

    package org.example.angst;
    
    import org.mockito.Mock;
    import org.mockito.MockitoAnnotations;
    import org.testng.Assert;
    import org.testng.annotations.BeforeClass;
    import org.testng.annotations.Test;
    
    import static org.mockito.Mockito.*;
    
    public class PersonTest {
        @Mock
        private UserSearchServiceImpl userSearchService;
        private final Person actualResult = new Person("amy", 18, "girls");
    
        @BeforeClass
        public void before() {
            MockitoAnnotations.initMocks(this);
        }
    
        /**
         * 首先定义Person类
         * 定义一个UserSearchService接口,定义个getInfo()方法
         * UserSearchServiceImpl实现该接口,重写getInfo()方法,返回一个Person对象
         */
        @Test(description = "正常mock实现类接口")
        public void testNormalAnalogCall (){
            //1.模拟 getInfo() 方法返回值
            when(userSearchService.getInfo()).thenReturn(new Person("amy", 18, "girls"));
            //2.调用接口
            Person response = userSearchService.getInfo();
            //3.断言接口返回和actualResult是否相等
            Assert.assertEquals(response.getName(), actualResult.getName());
        }
    }
    
    
    2.回调接口场景

    本文的重点来了!有些时候需要测试有回调接口函数,一般来说它们是异步执行的。很显然测试起来并不那么轻松,如果使用Thread.sleep(milliseconds)来等待它们执行完成只能说是一种比较low的实现,并且会让你的测试具有不确定性。这时候就体现 Mockito 的强大之处.

    例:假设我们有一个实现了 DummyCallback 接口的 DummyCallbackImpl,在 DummyCallbackImpl 中有一个doSomethingAsynchronously()方法,该方法会调用构造方法中传入的 DummyCollaborator对象,并调用其 doSomethingAsynchronously(DummyCallback callback),而它的任务在后台线程中执行完成之后就会回调这个callback 对象的 onSuccess() 方法。

    下面直接看示例:
    DummyCallback.java

    package org.example.angst;
    
    import java.util.List;
    
    /**
    * 提供了两个抽象方法
    * void onSuccess(List<String> result);
    * void onFail(int code);
    */
    public interface DummyCallback {
        void onSuccess(List<String> result);
        void onFail(int code);
    }
    

    DummyCallbackImpl.java

    package org.example.angst;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
    * 实现了 DummyCallback 接口,构造方法入参为 DummyCollaborator 对象
    * 提供了doSomethingAsynchronously() 方法默认传入this
    */
    public class DummyCallbackImpl implements DummyCallback {
    
        private final DummyCollaborator dummyCollaborator;
        private  List<String> result = new ArrayList<>();
    
        public DummyCallbackImpl(DummyCollaborator dummyCollaborator) {
            this.dummyCollaborator = dummyCollaborator;
        }
    
        public void doSomethingAsynchronously() {
            dummyCollaborator.doSomethingAsynchronously(this);
        }
    
        public List<String> getResult() {
            return this.result;
        }
    
        @Override
        public void onSuccess(List<String> result) {
            this.result = result;
            System.out.println("On success");
    
        }
    
        @Override
        public void onFail(int code) {
            System.out.println("On Fail"+ code);
        }
    }
    

    DummyCollaborator.java

    package org.example.angst;
    
    import static java.util.Collections.EMPTY_LIST;
    
    /**
    * 异步执行操作类,定义 `doSomethingAsynchronously()` 方法,
    * 开启线程回调 `DummyCallback` 对象的两个方法
    */
    public class DummyCollaborator {
        public static int ERROR_CODE = 1;
    
        public void doSomethingAsynchronously (final DummyCallback callback) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(5000);
                        callback.onSuccess(EMPTY_LIST);
                    } catch (InterruptedException e) {
                        callback.onFail(ERROR_CODE);
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    
    定义测试类

    下面会提供2种不同的方法来测试定义好的回调接口,但是首先我们先创建一个DummyCollaboratorCallerTest 测试类。

    package org.example.angst;
    
    import org.mockito.ArgumentCaptor;
    import org.mockito.Captor;
    import org.mockito.Mock;
    import org.mockito.MockitoAnnotations;
    import org.mockito.invocation.InvocationOnMock;
    import org.mockito.stubbing.Answer;
    import org.testng.Assert;
    import org.testng.annotations.BeforeClass;
    import org.testng.annotations.Test;
    import java.util.Arrays;
    import java.util.List;
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.Mockito.*;
    
    
    public class DummyCollaboratorCallerTest {
        @Mock
        private DummyCallbackImpl dummyCallbackImpl;
        @Mock
        private DummyCollaborator mockDummyCollaborator;
        @Captor
        private ArgumentCaptor<DummyCallback> dummyCallbackArgumentCaptor;
    
        @BeforeClass
        public void before() {
            MockitoAnnotations.initMocks(this);
            this.dummyCallbackImpl = new DummyCallbackImpl(mockDummyCollaborator);
        }
    
        @Test
        public void testDoSomethingAsynchronouslyUsingDoAnswer() {}
    
        @Test
        public void testDoSomethingAsynchronouslyUsingArgumentCaptor() {}
    }
    
    doAnswer 测试回调接口
        /**
         * 这是我们使用doAnswer()来为一个函数进行打桩以测试异步函数的测试用例。这意味着我们需要理解返回一个回调(同步的),
         * 当被测试的方法被调用时我们生成了一个通用的 answer,这个回调会被执行。
         * 最后,我们调用了doSomethingAsynchronously函数,并且验证了状态和交互结果。
         */
        @Test(description = "doAnswer 测试回调接口")
        public void testDoSomethingAsynchronouslyUsingDoAnswer() {
            // 1.为callback执行一个同步 answer
            final List<String> results = Arrays.asList("One", "Two", "Three");
            doAnswer(new Answer() {
                @Override
                public Object answer(InvocationOnMock invocation) {
                    ((DummyCallback)invocation.getArguments()[0]).onSuccess(results);
                    return null;
                }
            }).when(mockDummyCollaborator).doSomethingAsynchronously(any(DummyCallback.class));
            // 2.调用被测试的函数
            dummyCallbackImpl.doSomethingAsynchronously();
            // 3.验证状态与结果
            verify(mockDummyCollaborator, times(1)).doSomethingAsynchronously(any(DummyCallback.class));
            Assert.assertEquals(dummyCallbackImpl.getResult(), results);
        }
    
    ArgumentCaptor 测试异步回调接口
        /**
         *第二种实现是使用ArgumentCaptor。在这里我们的callback是异步的: 我们通过ArgumentCaptor捕获传递到DummyCollaborator对象的DummyCallback回调
         * 最终,我们可以在测试函数级别进行所有验证,当我们想验证状态和交互结果时可以调用 onSuccess()
         */
        @Test(description = "ArgumentCaptor 测试异步回调接口")
        public void testDoSomethingAsynchronouslyUsingArgumentCaptor() {
            final List<String> results = Arrays.asList("One", "Two", "Three");
            // 1.调用要被测试发函数
            dummyCallbackImpl.doSomethingAsynchronously();
            // 2.Let's call the callback. ArgumentCaptor.capture() works like a matcher.
            verify(mockDummyCollaborator, times(1)).doSomethingAsynchronously(
                    dummyCallbackArgumentCaptor.capture());
            // 3.在执行回调之前验证结果
            Assert.assertTrue(dummyCallbackImpl.getResult().isEmpty());
            // 4.调用回调的onSuccess函数
            dummyCallbackArgumentCaptor.getValue().onSuccess(results);
            // 5.再次验证结果
            Assert.assertEquals(dummyCallbackImpl.getResult(), results);
        }
    

    Epilogue

    以上两种实现的主要的不同点是在当使用 DoAnswer() 方案时我们创建了一个匿名内部类,并且将它的元素从invocation.getArguments()[n]转换到我们需要的类型,当万一这个类型匹配失败,那么对应用例也会失败。另一方面,当我们使用 ArgumentCaptor 时我们可能能够更精准的控制测试用例,因为我们能够捕获mock对象,并且能够通过手动来操作回调对象。以上两种方式虽然都能实现回调接口的mock,但是我更倾向于第二种。


    相关文章

      网友评论

        本文标题:单元测试 Mockito

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