zoukankan      html  css  js  c++  java
  • Mockito入门:如何在Spring中Mock部分对象

    前情提要

    随着分布式应用的开发逐渐成为标配,多个微服务团队合作来完成垂直业务的开发成为了一种常态。微服务使得团队可以专注于自己的业务逻辑,在和下游依赖和上游对接的团队聚焦好接口之后,就进入正式的开发。但是,每个团队的开发节奏往往不同,下游依赖所提供的服务有些时候不能在自测的时候提供稳定的服务。不仅是多个团队,单个团队中每个人所负责的模块之间也会存在依赖关系,也就同样存在这样的问题。

    这时候,就需要先在代码中模拟出依赖的服务,先确保自己开发的代码中的主流程能够跑通后。等下游依赖的服务发布后,再去除模拟的服务,用真实的服务测一遍。

    Mock服务可以依赖于一些框架来实现,最经典的就是Mockito。为什么最近专门来研究一下Mock对象的方法,是因为之前为了Mock下游服务直接修改了源代码中的实现。举个例子,本来应该从下游服务中根据用户ID获取用户的详情信息,包括用户名,用户年龄,用户性别等。但是因为用户中心的服务尚未发布,我直接修改了源代码中的实现中,返回了一个虚拟的用户信息。

    public Interface UserService{
      UserInfo getUser(String userId);
    }
    
    public Class UserServiceImpl implements UserService() {
        @Autowired
        private UserCenter userCenter;
        
        @Override
        public UserInfo getUser(String userId) {
            //注释了对下游服务的访问
            //return userCenter.getUser(userId);
            
            //创建了虚拟的用户信息并返回
            UserInfo userInfo = new UserInfo();
            userInfo.setUserName("xxx");
            ...
            return userInfo;
        }    
    }

    紧接着,问题来了。在自测完成之后,我忘记了将源代码中的注释内容恢复,直接将Mock实现提交到了代码仓库中。因为这个服务不止我一个依赖方调用,导致别人在调用这个接口的时候发现无论怎么修改用户ID,获得的用户数据都是一样的。由此,我开始了解如何在不修改源代码的情况下,对服务进行Mock,避免下一次再出现这样的问题。

    Mockito

    Mockito是Java单元测试中使用率最高的Mock框架之一。它通过简明的语法和完整的文档吸引了大量的开发者。Mockito支持用Maven和Gradle来进行依赖引入和管理。这里只给出Maven中引入依赖的例子:

            <dependency>
                <groupId>org.mockito</groupId>
                <artifactId>mockito-all</artifactId>
                <scope>test</scope>
            </dependency>

    下文以JUnit和Mockito两个框架作为基础进行详细说明

    需要测试的Service

    依赖的服务1,name方法会返回名称

    public interface ReliedService {
    
        String name();
    }
    
    @Service
    public class ReliedServiceImpl implements ReliedService {
    
        @Override
        public String name() {
            return "rale";
        }
    }

    依赖的服务2,welcome方法会返回欢迎语

    public interface WelcomeLanguageService {
    
        String welcome();
    }
    
    @Service
    public class WelcomeLanguageServiceImpl implements WelcomeLanguageService {
        @Override
        public String welcome() {
            return "wow";
        }
    }

    需要进行测试的服务DemoService。

    public interface DemoService {
    
        String hello();
    }
    
    @Service
    public class DemoServiceImpl implements DemoService{
    
        private ReliedService reliedService;
    
        private WelcomeLanguageService welcomeLanguageService;
    
        @Override
        public String hello() {
            return welcomeLanguageService.welcome() + " " + reliedService.name();
        }
    
        //之所以采用setter的方式进行依赖注入,是为了实现Mock对象的注入
        @Autowired
        public void setReliedService(ReliedService reliedService) {
            this.reliedService = reliedService;
        }
    
        @Autowired
        public void setWelcomeLanguageService(WelcomeLanguageService welcomeLanguageService) {
            this.welcomeLanguageService = welcomeLanguageService;
        }
    }

    开启Mock

    方法1. Mockito.mock

    直接使用Mockito提供的mock方法即可以模拟出一个服务的实例。再结合when/thenReturn等语法完成方法的模拟实现。

    import static org.mockito.Mockito.*;
    
    @DelegateTo(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = { Application.class })
    public class MockDemo1 {
    
        private DemoService demoService;
    
        @Before
        public void before() {
            demoService = mock(DemoService.class);
        }
    
        @Test
        public void test() {
            when(demoService.hello()).thenReturn("hello my friend");
            System.out.println(demoService.hello());
            verify(demoService).hello();
        }
    }

    方法2. MockitoAnnotations.initMocks(this)

    这里给出了使用@Mock注解来Mock对象时的第一种实现,即使用MockitoAnnotations.initMocks(testClass)。

    @DelegateTo(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = { Application.class })
    public class MockDemo2 {
    
        @Mock
        private DemoService demoService;
    
        @Before
        public void before() {
            MockitoAnnotations.initMocks(this);
        }
    
        @Test
        public void test() {
            when(demoService.hello()).thenReturn("hello rale");
            System.out.println(demoService.hello());
            verify(demoService).hello();
        }
    }

    方法3. @RunWith(MockitoJUnitRunner.class)(推荐)

    在测试用例上带上了这个注解后,就可以自由的使用@Mock来Mock对象啦。

    @RunWith(MockitoJUnitRunner.class)
    @DelegateTo(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = { Application.class })
    public class MockDemo3 {
    
        @Mock
        private DemoService demoService;
    
        @Test
        public void test() {
            when(demoService.hello()).thenReturn("hello rale");
            System.out.println(demoService.hello());
            verify(demoService).hello();
        }
    }

    方法4. MockitoRule

    这里需要注意的是如果使用MockitoRule的话,该对象的访问级别必须为public。

    @RunWith(JUnit4.class)
    @DelegateTo(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = { Application.class })
    public class MockDemo4 {
    
        @Rule
        public MockitoRule rule = MockitoJUnit.rule();
    
        @Mock
        private DemoService demoService;
    
        @Test
        public void test() {
            when(demoService.hello()).thenReturn("hello rale");
            System.out.println(demoService.hello());
            verify(demoService).hello();
        }
    }

    在上面四种方法中,最推荐的就是第二种方法,如果无法使用@RunWith(MockitoJUnitRunner.class)时,再考虑别的兼容的方法。

    Stub

    标准的Stub在上文中已经给出了简单的例子,目前Mockito基于BDD(Behavior Driven Development)的思想还提供了类似的given/willReturn的语法。但是,Spring同样作为IOC框架,和Mockito的融合存在一定的问题。即如果需要对Spring Bean中的部分依赖进行Stub时,需要手动的去设置。

    Mockito其实提供了一个非常方便的注解叫做@InjectMocks,该注解会自动把该单元测试中声明的Mock对象注入到该Bean中。但是,我在实验的过程中遇到了问题,即@InjectMocks如果想要标记在接口上,则该接口必须手动初始化,否则会抛出无法初始化接口的异常。但是,如果不使用Spring的自动注入,则必须手动的将该类依赖的别的Bean注入进去。

    因此目前使用Mockito的妥协方案是直接@Autowire该接口的实现。然后在上面标记InjectMocks注解,此时会将测试中声明的Mock对象自动注入,而没有声明的依赖的对象依然采用Spring Bean的依赖注入:

    @RunWith(MockitoJUnitRunner.class)
    @DelegateTo(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = { Application.class })
    public class InjectMockTest {
    
        @Mock
        private WelcomeLanguageService welcomeLanguageService;
    
        @Autowired
        @InjectMocks
        private DemoServiceImpl demoService;
    
        @Before
        public void before() {
            MockitoAnnotations.initMocks(this);
            given(welcomeLanguageService.welcome()).willReturn("hahaha");
        }
        @Test
        public void test() {
            System.out.println(demoService.hello());
        }
    }

    DemoService中,WelcomeLanguageService会使用Mock对象,而ReliedService会使用Spring Bean自动注入。

    参考文章

    Mockito官方文档

  • 相关阅读:
    一条痛并快乐的路
    Daily Scrum 11.1
    Daily Scrum 10.31
    Daily Scrum 10.30
    Daily Scrum 10.29
    Daily Scrum 10.28
    Daily Scrum 10.27
    (Alpha)Let's-Chronos分数分配规则
    Daily Scrum 10.26
    Daily Scrum 10.25
  • 原文地址:https://www.cnblogs.com/exmyth/p/12535900.html
Copyright © 2011-2022 走看看