Spring 结合 Mockito 编写单元测试


#Spring#


Mockito 是一个模拟测试框架。主要功能是模拟类/对象的行为。Mockito 一般用于控制调用外部的返回值,让我们只关心和测试自己的业务逻辑。

关于 Mockito 的具体使用方法可以参考 Mocktio 入门

我们用一个示例说明 Spring 中如何使用 Mockito 进行测试。

示例

项目结构

.
├── build.gradle
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── demo
    │   │       ├── AppConfig.java
    │   │       ├── BusinessService.java
    │   │       ├── HttpService.java
    │   │       └── Main.java
    │   └── resources
    └── test
        └── java
            └── demo
                ├── BusinessServiceTest01.java
                └── BusinessServiceTest02.java

build.gradle

build.gradle 中配置的依赖如下:

dependencies {

    compile group: 'org.springframework', name: 'spring-context', version: '5.0.6.RELEASE'

    testCompile group: 'org.springframework', name: 'spring-test', version: '5.0.6.RELEASE'
    testCompile group: 'junit', name: 'junit', version: '4.12'
    testCompile group: 'org.mockito', name: 'mockito-core', version: '2.25.1'

}

src/main/java/demo 中是一个简单的 Spring 项目,各个类的内容如下:

HttpService.java

package demo;

import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class HttpService {

    public int queryStatus() {
        // 发起网络请求,得到响应值,然后返回
        // 这里用随机数模拟
        return new Random().nextInt(2);
    }

}

BusinessService.java

package demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BusinessService {

    @Autowired
    private HttpService httpService;

    public String hello() {
        int status = httpService.queryStatus();
        if (status == 0) {
            return "你好";
        }
        else {
            return "Hello";
        }
    }

}

AppConfig.java

package demo;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class AppConfig {

}

Main.java

package demo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        BusinessService businessService = applicationContext.getBean(BusinessService.class);
        System.out.println(businessService.hello()); // 输出"你好",或者"Hello"

    }
}

src/test/java/demo 编写了两个关于 BusinessService 的测试类。

测试示例1:使用 ReflectionTestUtils 设置 mock 对象

BusinessServiceTest01 类内容如下:

package demo;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.util.ReflectionTestUtils;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AppConfig.class)
public class BusinessServiceTest01 {

    @Autowired
    private BusinessService businessService;

    @Test
    public void testHello() {
        // 取出真实的 httpService
        HttpService realHttpService = (HttpService) ReflectionTestUtils.getField(businessService, "httpService");

        // 生成 HttpService 的 mock 对象,替换掉 businessService 的真实对象
        HttpService mockHttpService = Mockito.mock(HttpService.class);
        ReflectionTestUtils.setField(businessService, "httpService", mockHttpService);

        // 给 mock 对象打桩
        Mockito.when(mockHttpService.queryStatus()).thenReturn(0);

        // 测试
        Assert.assertEquals("你好", businessService.hello());  // 永远都返回 "你好"

        // 恢复真实的 httpService
        ReflectionTestUtils.setField(businessService, "httpService", realHttpService);
    }

}

这种使用方式过于繁琐,下面的示例,更简洁。

测试示例2:使用 @Mock、@InjectMocks 自动注入 mock 对象

package demo;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.util.ReflectionTestUtils;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AppConfig.class)
public class BusinessServiceTest02 {

    @InjectMocks
    @Autowired
    private BusinessService businessService;

    @Mock
    private HttpService mockHttpService;  

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this);  // 这个必须要有
    }

    @Test
    public void testHello() {
        // 给 mock 对象打桩
        Mockito.when(mockHttpService.queryStatus()).thenReturn(0);
        // 测试
        Assert.assertEquals("你好", businessService.hello());  // 永远都返回 "你好"
    }

}

这种使用方式更加简洁。

但要注意:

第1点: businessService 是一个 Spring 单例 Bean,其中的真实 httpService 被 Mockito 替换成了 mock 对象。在较大的项目中会有很多单元测试用例,如果 BusinessServiceTest02 之后还有测试类要用到 businessService,它内部的 httpService 都会是 mock 对象。

第2点:

如果 BusinessService 内部使用了 @Transcational 或者接入了切面,那么 businessService 对象会是一个代理对象,此时,@InjectMocks 的方式是无效的。因为只是将 mock 对象注入到了代理对象中,没有注入到被代理对象中。

怎么办?用示例1中的ReflectionTestUtils.setField,该方法会将 mock 对象注入到被代理对象中。ReflectionTestUtils.setField在较旧的 spring-test 版本中不支持注入到被代理对象。判断是否支持,看 setField 源码中有没有下面的内容:

if (targetObject != null && springAopPresent) {
    targetObject = AopTestUtils.getUltimateTargetObject(targetObject);
}

如果因为一些原因无法升级 Spring 来解决这种问题,可以参考 UT中使用ReflectionTestUtils.setField不能mock掉依赖问题解决


( 本文完 )