在web项目进行单元测试时,我们希望测试controller或者service的功能,同时不会实际调用Dao层代码完成数据库交互。也就是只执行controller或者service本身的代码,但不实际调用Dao的方法,这种场景就需要使用Mock技术。
目前Java中常见的Mock框架有Mockito和PowerMock,其中PowerMock对Mockito的功能进行了增强,比如可以Mock私有方法、final方法等。Mockito和PowerMock的API类似,这里只介绍Mockito的用法。
单元测试使用的项目代码参考:SpringBoot+Mybatis Demo搭建
controller
代码如下:
@RestController
public class CityRestController {
private final Logger LOG = LoggerFactory.getLogger(CityRestController.class);
@Autowired
private CityService cityService;
@RequestMapping(value = "/api/city", method = RequestMethod.GET)
public City findOneCity(@RequestParam(value = "cityName", required = true) String cityName) {
LOG.debug(">>>>>>>>>>Enter Controller");
LOG.info(">>>>>>>>>>Enter Controller");
LOG.warn(">>>>>>>>>>Enter Controller");
LOG.error(">>>>>>>>>>Enter Controller");
return cityService.findCityByName(cityName);
}
}
复制代码
service
接口及实现代码如下:
public interface CityService {
City findCityByName(String cityName);
}
复制代码
@Service
public class CityServiceImpl implements CityService {
private final Logger LOG = LoggerFactory.getLogger(CityServiceImpl.class);
@Autowired
private CityDao cityDao;
public City findCityByName(String cityName) {
LOG.debug(">>>>>>>>>>Enter Servie");
LOG.info(">>>>>>>>>>Enter Servie");
LOG.warn(">>>>>>>>>>Enter Servie");
LOG.error(">>>>>>>>>>Enter Servie");
return cityDao.findByName(cityName);
}
}
复制代码
接下来就说明如何对controller
进行单元测试。
1 创建Mock对象
1.1 使用@Mock
和@InjectMock
@Mock
创建Mock对象。既可以Mock类也可以Mock接口。
@InjectMocks
创建1个对象实例并向其中注入由@Mock
或者 @Spy
注解创建Mock对象。由于会创建对象实例,因此只能Mock类,不能Mock抽象类和接口。
使用@Mock
和@InjectMock
时,在Junit4中还必须使用@RunWith(MockitoJUnitRunner.class)
或者MockitoAnnotations.openMocks(this)
真正完成Mock对象创建以及注入。在JUnit 5则必须使用@ExtendWith(MockitoExtension.class)
。
1.1.1 @RunWith
初始化
@RunWith(MockitoJUnitRunner.class)
public class CityRestControllerTestMockAnno {
@Mock
private CityService cityService;
@InjectMocks
private CityRestController cityRestController;
@Test
public void findOneCityTest_doReturn_When() {
City city = new City();
city.setCityName("Beijing");
Mockito.doReturn(city).when(cityService).findCityByName(Mockito.anyString());
City mockResult = cityRestController.findOneCity("UnitTest");
Assert.assertEquals(city.getCityName(), mockResult.getCityName());
}
}
复制代码
1.1.2 openMock()
初始化
首先定义基类
public class BaseTest {
private final Logger LOG = LoggerFactory.getLogger(BaseTest.class);
@Before
public void init() {
LOG.info(">>>>>openMock invoked>>>>>");
MockitoAnnotations.openMocks(this);
}
}
复制代码
然后继承该基类
public class CityRestControllerTestMockAnnoExtend extends BaseTest {
@Mock
private CityService cityService;
@InjectMocks
private CityRestController cityRestController;
@Test
public void findOneCityTest_doReturn_When() {
City city = new City();
city.setCityName("Beijing");
Mockito.doReturn(city).when(cityService).findCityByName(Mockito.anyString());
City mockResult = cityRestController.findOneCity("UnitTest");
Assert.assertEquals(city.getCityName(), mockResult.getCityName());
}
}
复制代码
1.2 不用@Mock
和@InjectMock
除了使用@Mock
和@InjectMock
来创建Mock对象cityService
并注入到待测试的类对象实例cityRestController
外,还可以使用Mockito的API接口来完成。由于cityRestController
存在私有属性cityService
,要把创建的Mock对象注入其中,要么通过反射机制来实现,要么就给ontroller
增加public
的setter
方法手动完成设置。
1.2.1 利用反射机制
public class CityRestControllerTestMockMethod {
private CityService cityService;
private CityRestController cityRestController;
@Test
public void findOneCityTest_setMethod() {
// 等价于@Mock
cityService = Mockito.mock(CityService.class);
// 等价于@InjectMock
cityRestController = new CityRestController();
Class<?> clazz = Class.forName("org.spring.springboot.controller.CityRestController");
Field field = clazz.getDeclaredField("cityService");
field.setAccessible(true);
field.set(cityRestController, cityService);
City city = new City();
city.setCityName("Beijing");
Mockito.doReturn(city).when(cityService).findCityByName(Mockito.anyString());
City mockResult = cityRestController.findOneCity("UnitTest");
Assert.assertEquals(city.getCityName(), mockResult.getCityName());
}
}
复制代码
1.2.2 增加setter方法
首先在controller中增加setCityService
方法
@RestController
public class CityRestController {
private final Logger LOG = LoggerFactory.getLogger(CityRestController.class);
// @Autowired
private CityService cityService;
// 增加setCityService方法
@Autowired
public void setCityService(CityService cityService) {
this.cityService = cityService;
}
// 省略原来代码
}
复制代码
单元测试代码如下:
public class CityRestControllerTestMockMethod {
private CityService cityService;
private CityRestController cityRestController;
@Test
public void findOneCityTest_setMethod() {
// 等价于@Mock
cityService = Mockito.mock(CityService.class);
// 等价于@InjectMock
cityRestController = new CityRestController();
cityRestController.setCityService(cityService);
City city = new City();
city.setCityName("Beijing");
Mockito.doReturn(city).when(cityService).findCityByName(Mockito.anyString());
City mockResult = cityRestController.findOneCity("UnitTest");
Assert.assertEquals(city.getCityName(), mockResult.getCityName());
}
}
复制代码
2 Mock方法
2.1 doReturn().when()
vs when().thenReturn()
doReturn()
的参数类型为Object
,在编译阶段不会进行类型校验,因此运行时如果类型不匹配则会抛异常。而thenReturn()
则时在编译阶段就进行类型检查,可以提前发现问题。- 对于通过
@Mock
或者Mockioto.mock()
创建的Mock对象,doReturn().when()
和when().thenReturn()
都不会实际调用被Mock对象的方法,直接方法指定的结果,都不存在副作用,两种方法的效果完全一致。 - 对于通过
@Spy
或者Mockioto.spy()
创建的Spy对象,doReturn().when()
依然不会实际调用被Modk对象的方法,但是when().thenReturn()
则会,也就可能产生副作用。
@RunWith(MockitoJUnitRunner.class)
public class CityRestControllerTestSpyAnno {
private final Logger LOG = LoggerFactory.getLogger(CityRestControllerTestSpyAnno.class);
@Spy
private CityService cityService;
@InjectMocks
private CityRestController cityRestController;
@Test
public void findOneCityTest_doReturn_When() {
LOG.info(">>>>>doReturn().when() invoked>>>>>");
City city = new City();
city.setCityName("Beijing");
Mockito.doReturn(city).when(cityService).findCityByName(Mockito.anyString());
City mockResult = cityRestController.findOneCity("UnitTest");
Assert.assertEquals(city.getCityName(), mockResult.getCityName());
}
@Test
public void findOneCityTest_When_thenReturn() {
LOG.info(">>>>>when().thenReturn() invoked>>>>>");
City city = new City();
city.setCityName("Beijing");
Mockito.when(cityService.findCityByName(Mockito.anyString())).thenReturn(city);
City mockResult = cityRestController.findOneCity("UnitTest");
Assert.assertEquals(city.getCityName(), mockResult.getCityName());
}
}
复制代码
执行结果:
发现两种方式的测试结果相同,这是由于@Spy
作用在接口CityService
上,而接口方法本身没有什么输出标识,所以看不出来区别。将单元测试代码中@Spy private CityService cityService;
改为@Spy private CityServiceImpl cityService;
。
执行结果如下,可以看到when().thenReturn()
确实调用了CityServiceImpl
的方法。而且中日志还可以看到在when().thenReturn()
就直接调用了CityServiceImpl
的方法,并没有等到cityRestController.findOneCity()
来触发。至于测试报错,则是有CityServiceImpl
中cityDao
没有初始化导致的,这就是另外1个问题。
2.2 doNothing().when()
用来Mock返回值为void
的方法。
2.3 doThrow().when()
用来Mock方法调用时抛出指定异常。
2.4 doCallRealMethod().when()
用来Mock方法调用时调用原始的方法。
2.5 doAnswer().when()
用于Mock方法调用时,根据不同的参数指定不同的返回结果。
3 Mock static方法
在单元测试中,我们常常需要mock工具类的static方法,在Mockito 3.4.0前需要借助PowerMock来实现,在3.4.0版本后Mockito自身也支持Mock static方法。
待测试类:
public class StaticUtils {
private StaticUtils() {}
public static List<Integer> range(int start, int end) {
return IntStream.range(start, end)
.boxed()
.collect(Collectors.toList());
}
public static String name() {
return "Baeldung";
}
}
复制代码
Mock不带参数的static方法:
@Test
void givenStaticMethodWithNoArgs_whenMocked_thenReturnsMockSuccessfully() {
assertThat(StaticUtils.name()).isEqualTo("Baeldung");
try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
utilities.when(StaticUtils::name).thenReturn("Eugen");
assertThat(StaticUtils.name()).isEqualTo("Eugen");
}
assertThat(StaticUtils.name()).isEqualTo("Baeldung");
}
复制代码
通过Mockito.mockStatic()
创建scoped mock对象。创建的scoped mock对象在使用完成后必须关闭,否则会一直存在于创建线程中,关闭方法:创建的scoped mock.close()
。
Mock带参数的static方法:
@Test
void givenStaticMethodWithArgs_whenMocked_thenReturnsMockSuccessfully() {
assertThat(StaticUtils.range(2, 6)).containsExactly(2, 3, 4, 5);
try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
utilities.when(() -> StaticUtils.range(2, 6))
.thenReturn(Arrays.asList(10, 11, 12));
assertThat(StaticUtils.range(2, 6)).containsExactly(10, 11, 12);
}
assertThat(StaticUtils.range(2, 6)).containsExactly(2, 3, 4, 5);
}
复制代码
参考资料:Mocking Static Methods With Mockito
4 Mock private方法
Mockito不支持Mock private方法,需要通过PowerMock来实现。
Mock技术应该用来Mock被测试类的外部依赖而不是类本身。 如果本测试类的单元测试必须要Mock private方法,说明类的设计可能不够合理。