美文网首页
关于Mock、Spy、@MockBean、@SpyBean的笔记

关于Mock、Spy、@MockBean、@SpyBean的笔记

作者: 花雨归来 | 来源:发表于2023-04-13 07:47 被阅读0次

    前言

    1. Mock是将目标对象整个模拟 ,所有方法默认都返回null,并且原方法中的代码逻辑不会执行,被Mock出来的对象,想用哪个方法,哪个方法就需要打桩,否则返回null
    2. Spy可实现对目标对象部分方法、特定入参条件时的打桩,没有被打桩的方法,将会真实调用。

    本文maven依赖

    本文使用了SpringJunit5Mokitocommons-lang3工具类。

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    

    1. 非Spring环境

    假设我们有如下服务:

    public class DemoService {
        public String serviceA(String name) {
            System.out.println("serviceA 被真实调用了,当前入参:" + name);
            return "serviceA 真实返回:" + name;
        }
    
        public String serviceB(String name) {
            System.out.println("serviceB 被真实调用了,当前入参:" + name);
            return "serviceB 真实返回:" + name;
        }
    }
    

    我们用如下单测代码示例,注释已经很清楚了,此处不再赘述。

    import org.apache.commons.lang3.RandomStringUtils;
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.mockito.Mockito;
    
    class DemoServiceTest {
    
        @Test
        @DisplayName("Mock的正确姿势")
        void testMock() {
            // Mock 出一个目标对象的实例,注意不是 new 出来的真实对象
            DemoService mockService = Mockito.mock(DemoService.class);
    
            // 假设我们仅对 serviceA 方法,并且入参等于 "asdf" 时进行打桩
            Mockito.when(mockService.serviceA(Mockito.eq("asdf")))
                    .thenReturn("mock:serviceA方法入参等于asdf时的特定返回值");
    
            // 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "mock:serviceA方法入参等于asdf时的特定返回值"
            String asdf = mockService.serviceA("asdf");
            System.out.println("asdf = " + asdf);
            Assertions.assertEquals("mock:serviceA方法入参等于asdf时的特定返回值", asdf);
    
            // 虽然也是调用 serviceA 方法,但由于没有命中打桩规则,所以返回值是 null
            String qwer = mockService.serviceA("qwer");
            System.out.println("qwer = " + qwer);
            Assertions.assertNull(qwer);
    
            // serviceB 根本没有打桩,但由于 mockService 这个对象实例是 Mock 出来的,
            // 所以 serviceB 的方法体代码不会被执行,并且返回值固定为 null,不管入参是什么
            for (int i = 0; i < 3; i++) {
                String returnValueFromRandom = mockService.serviceB(RandomStringUtils.random(10));
                System.out.println("returnValueFromRandom 第[" + i + "]次 = " + returnValueFromRandom);
                Assertions.assertNull(returnValueFromRandom);
            }
        }
    
        @Test
        @DisplayName("Spy的错误打桩姿势")
        void testBadSpy() {
            // Spy 一个目标对象的实例,注意不是 new 出来的 100% 真实的实例,也不是 100% 假的实例
            // 到底有多真,有多假,取决于打桩埋点的覆盖程度
            DemoService spyService = Mockito.spy(DemoService.class);
    
            // 打桩错误示例:参照 Mock 打桩的写法,预期对 serviceA 方法打桩,
            // 并且当入参等于 "asdf" 时,返回特定值 "spy:serviceA方法入参等于asdf时的特定返回值"
            Mockito.when(spyService.serviceA(Mockito.eq("asdf")))
                    .thenReturn("spy:serviceA方法入参等于asdf时的特定返回值");
    
            // 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "spy:serviceA方法入参等于asdf时的特定返回值"
            String asdf = spyService.serviceA("asdf");
            System.out.println("asdf = " + asdf);
            Assertions.assertEquals("spy:serviceA方法入参等于asdf时的特定返回值", asdf);
            // 注意:上述对入参等于 "asdf" 时,被"spy错误打桩"的方法 serviceA 的返回值的断言,是可以跑通的
            // 但是,存在如下两个问题:
            // 1. serviceA 被真实调用了;(这一点往往不是我们的预期)
            // 2. serviceA 被真实调用时,它的实际入参,其实并不是 "asdf",而是 null。
            // (想象一下如果 serviceA 方法体内真正运行时,极有可能由于实际入参是 null 而抛出异常中断执行,这是一种灾难)
    
            // 下述调用,虽然 serviceA 被打桩,由于 "qwer" 并没有命中打桩点,所以它不会返回打桩点设定的返回值
            // 不同于 Mock 出来的对象,下述调用会真实地执行 serviceA 方法体代码块,而不是像 Mock 一样返回 null
            System.out.println("==============================================");
            String qwer = spyService.serviceA("qwer");
            System.out.println("qwer = " + qwer);
            Assertions.assertEquals("serviceA 真实返回:qwer", qwer);
    
            // 同理,serviceB 方法没有被打桩,但由于 spyService 是 Spy 出来的对象实例,
            // 不同于 Mock 出来的对象实例返回 null,对 serviceB 的调用会真实的执行目标方法,并按真实情况返回
            System.out.println("==============================================");
            String zxcv = spyService.serviceB("zxcv");
            System.out.println("zxcv = " + zxcv);
            Assertions.assertEquals("serviceB 真实返回:zxcv", zxcv);
        }
    
        @Test
        @DisplayName("Spy的正确打桩姿势")
        void testGoodSpy() {
            // 同样,先用 Spy 方式创建一个目标对象的实例
            DemoService spyService = Mockito.spy(DemoService.class);
    
            // 对 Spy 对象的正确打桩姿势,
            // (巧了,用这种 doReturn|doThrow|doAnswer(xxx).when(obj).methodXXX() 的方式,也适用于 Mock 出来的对象)
            Mockito.doReturn("spy:serviceA方法入参等于asdf时的特定返回值2")
                    // 注意这里 when 里面是对象实例的变量名,而不是一个 methodCall
                    .when(spyService)
                    // 再通过 when 泛型方法返回的Spy实例引用,对目标方法打桩
                    .serviceA(Mockito.eq("asdf"));
    
            // 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "spy:serviceA方法入参等于asdf时的特定返回值2"
            String asdf = spyService.serviceA("asdf");
            System.out.println("asdf = " + asdf);
            Assertions.assertEquals("spy:serviceA方法入参等于asdf时的特定返回值2", asdf);
            // 说明:由于上面正确的打桩姿势,上述被命中打桩规则的调用,不会触发目标方法的真实调用,不存在上述错误用法的2个问题。
    
            // 对于调用打桩方法,但没有命中打桩入参条件的调用,表现行为与上述 testBadSpy 单测代码一样
            System.out.println("==============================================");
            String qwer = spyService.serviceA("qwer");
            System.out.println("qwer = " + qwer);
            Assertions.assertEquals("serviceA 真实返回:qwer", qwer);
    
            // 同理,对于没有打桩的 spy 对象实例的其他方法,表现行为与上述 testBadSpy 单测代码一样
            System.out.println("==============================================");
            String zxcv = spyService.serviceB("zxcv");
            System.out.println("zxcv = " + zxcv);
            Assertions.assertEquals("serviceB 真实返回:zxcv", zxcv);
        }
    }
    

    2. Spring环境

    假设我们有如下服务:

    import org.springframework.stereotype.Component;
    
    @Component
    public class DemoBeanService {
        public String serviceA(String name) {
            System.out.println("bean serviceA 被真实调用了,当前入参:" + name);
            return "bean serviceA 真实返回:" + name;
        }
    
        public String serviceB(String name) {
            System.out.println("bean serviceB 被真实调用了,当前入参:" + name);
            return "bean serviceB 真实返回:" + name;
        }
    }
    

    注意:由于Spring IoC默认是单例的,为了区分,我们分别为@MockBean@SpyBean创建单独的测试类举例。

    @MockBean示例

    import org.apache.commons.lang3.RandomStringUtils;
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.mockito.Mockito;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.test.context.ContextConfiguration;
    
    @ContextConfiguration(classes = DemoBeanService.class) // 此处我们的IoC只扫描注入 DemoBeanService 类
    @SpringBootTest
    public class MockBeanTest {
        @MockBean
        DemoBeanService mockBeanService;
    
        @Test
        @DisplayName("MockBean的正确姿势")
        void testMock() {
            // 不同于普通 Mock 方式,这里不需要调用 Mockito.mock(xxx.class) 创建 mock 对象实例
            // 我们已经通过 @MockBean 的方式,将一个 mock 对象的实例放入了 Spring IoC ApplicationContext 中
            // 注意到 Spring IoC 默认是单例的,也就是当前 ApplicationContext 中,只有一个 mock 出来的 DemoBeanService 实例
            // 所以,如果它打桩不全的话,在当前这个 IoC 中,调用没有被打桩的方法,将一律返回 null
            // 因此在不同的 xxxTest.java 中出现不同 MockBean 时,会触发 Spring 上下文重建,写的 MockBean 越多,整个工程单测就越慢
    
            // 假设我们仅对 serviceA 方法,并且入参等于 "asdf" 时进行打桩
            Mockito.when(mockBeanService.serviceA(Mockito.eq("asdf")))
                    .thenReturn("MockBean:serviceA方法入参等于asdf时的特定返回值");
    
            // 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "MockBean:serviceA方法入参等于asdf时的特定返回值"
            String asdf = mockBeanService.serviceA("asdf");
            System.out.println("asdf = " + asdf);
            Assertions.assertEquals("MockBean:serviceA方法入参等于asdf时的特定返回值", asdf);
    
            // 虽然也是调用 serviceA 方法,但由于没有命中打桩规则,所以返回值是 null
            String qwer = mockBeanService.serviceA("qwer");
            System.out.println("qwer = " + qwer);
            Assertions.assertNull(qwer);
    
            // serviceB 根本没有打桩,但由于 mockBeanService 这个对象实例是 Mock 出来的,
            // 所以 serviceB 的方法体代码不会被执行,并且返回值固定为 null,不管入参是什么
            for (int i = 0; i < 3; i++) {
                String returnValueFromRandom = mockBeanService.serviceB(RandomStringUtils.random(10));
                System.out.println("returnValueFromRandom 第[" + i + "]次 = " + returnValueFromRandom);
                Assertions.assertNull(returnValueFromRandom);
            }
        }
    }
    

    @SpyBean示例

    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.mockito.Mockito;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.mock.mockito.SpyBean;
    import org.springframework.test.context.ContextConfiguration;
    
    @ContextConfiguration(classes = DemoBeanService.class)
    @SpringBootTest
    public class SpyBeanTest {
        @SpyBean
        DemoBeanService spyBeanService;
    
        @Test
        @DisplayName("SpyBean的错误打桩姿势")
        void testBadSpyBean() {
            // 与 MockBean 类似,此处也不再需要 Mockito.spy(xxx.class) 创建 spy 对象实例
            // IoC 中也同样有一个 spy 对象实例,详情参见 @MockBean 的Test示例
    
            // 打桩错误示例:参照 Mock 打桩的写法,预期对 serviceA 方法打桩,
            // 并且当入参等于 "asdf" 时,返回特定值 "SpyBean:serviceA方法入参等于asdf时的特定返回值"
            Mockito.when(spyBeanService.serviceA(Mockito.eq("asdf")))
                    .thenReturn("SpyBean:serviceA方法入参等于asdf时的特定返回值");
    
            // 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "SpyBean:serviceA方法入参等于asdf时的特定返回值"
            String asdf = spyBeanService.serviceA("asdf");
            System.out.println("asdf = " + asdf);
            Assertions.assertEquals("SpyBean:serviceA方法入参等于asdf时的特定返回值", asdf);
            // 注意:上述对入参等于 "asdf" 时,被"spy错误打桩"的方法 serviceA 的返回值的断言,是可以跑通的
            // 但是,存在如下两个问题:
            // 1. serviceA 被真实调用了;(这一点往往不是我们的预期)
            // 2. serviceA 被真实调用时,它的实际入参,其实并不是 "asdf",而是 null。
            // (想象一下如果 serviceA 方法体内真正运行时,极有可能由于实际入参是 null 而抛出异常中断执行,这是一种灾难)
    
            // 下述调用,虽然 serviceA 被打桩,由于 "qwer" 并没有命中打桩点,所以它不会返回打桩点设定的返回值
            // 不同于 Mock 出来的对象,下述调用会真实地执行 serviceA 方法体代码块,而不是像 Mock 一样返回 null
            System.out.println("==============================================");
            String qwer = spyBeanService.serviceA("qwer");
            System.out.println("qwer = " + qwer);
            Assertions.assertEquals("bean serviceA 真实返回:qwer", qwer);
    
            // 同理,serviceB 方法没有被打桩,但由于 spyService 是 Spy 出来的对象实例,
            // 不同于 Mock 出来的对象实例返回 null,对 serviceB 的调用会真实的执行目标方法,并按真实情况返回
            System.out.println("==============================================");
            String zxcv = spyBeanService.serviceB("zxcv");
            System.out.println("zxcv = " + zxcv);
            Assertions.assertEquals("bean serviceB 真实返回:zxcv", zxcv);
        }
    
        @Test
        @DisplayName("SpyBean的正确打桩姿势")
        // 注意:由于和 testBadSpyBean() 方法是分别打桩的(都是在各自的方法体局部打桩),因此不会互相影响
        void testGoodSpyBean() {
            // 当前测试方法(testGoodSpyBean),不使用 Mockito.spy(xxx.class) 创建 spy 对象实例
            // 同时,由于和 testBadSpyBean() 在一个测试类中,会共用同一个 spy 对象实例(DemoBeanService的instance)
    
            // 对 Spy 对象的正确打桩姿势,
            // (巧了,用这种 doReturn|doThrow|doAnswer(xxx).when(obj).methodXXX() 的方式,也适用于 Mock 出来的对象)
            Mockito.doReturn("SpyBean:serviceA方法入参等于asdf时的特定返回值3")
                    // 注意这里 when 里面是对象实例的变量名,而不是一个 methodCall
                    .when(spyBeanService)
                    // 再通过 when 泛型方法返回的Spy实例引用,对目标方法打桩
                    .serviceA(Mockito.eq("asdf"));
    
            // 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "SpyBean:serviceA方法入参等于asdf时的特定返回值3"
            String asdf = spyBeanService.serviceA("asdf");
            System.out.println("asdf = " + asdf);
            Assertions.assertEquals("SpyBean:serviceA方法入参等于asdf时的特定返回值3", asdf);
            // 说明:由于上面正确的打桩姿势,上述被命中打桩规则的调用,不会触发目标方法的真实调用,不存在上述错误用法的2个问题。
    
            // 对于调用打桩方法,但没有命中打桩入参条件的调用,表现行为与上述 testBadSpy 单测代码一样
            System.out.println("==============================================");
            String qwer = spyBeanService.serviceA("qwer");
            System.out.println("qwer = " + qwer);
            Assertions.assertEquals("bean serviceA 真实返回:qwer", qwer);
    
            // 同理,对于没有打桩的 spy 对象实例的其他方法,表现行为与上述 testBadSpy 单测代码一样
            System.out.println("==============================================");
            String zxcv = spyBeanService.serviceB("zxcv");
            System.out.println("zxcv = " + zxcv);
            Assertions.assertEquals("bean serviceB 真实返回:zxcv", zxcv);
        }
    }
    

    补充

    1. 其实,@MockBean@SpyBean注解,除了在FIELD上可以支持外,也可以直接在测试类上使用,适用范围可参考上述注解的源码@Target({ ElementType.TYPE, ElementType.FIELD })。因为这个漂亮的特性,我们可以通过设计一个测试基类BaseTest,让它的所有测试子类,最大程度地共享同一个IoC容器,而不是频繁触发重建ApplicationContext拖慢执行速度,从而提高整个工程的单测执行效率。
    2. 此外,在JAVA8以前,是可以使用@MockBeans@SpyBeans在同一个类上添加多个对应的@MockBean@SpyBean,在JAVA8以后,得益于@Repeatable注解,您可以直接在一个类上添加n个单一注解@MockBean@SpyBean

    相关文章

      网友评论

          本文标题:关于Mock、Spy、@MockBean、@SpyBean的笔记

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