乐趣区

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 官方文档

退出移动版