Java单元测试框架Mockito

在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增加publicsetter方法手动完成设置。

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());
    }
}
复制代码

执行结果:

image-20211130223829524.png

发现两种方式的测试结果相同,这是由于@Spy作用在接口CityService上,而接口方法本身没有什么输出标识,所以看不出来区别。将单元测试代码中@Spy private CityService cityService;改为@Spy private CityServiceImpl cityService;

执行结果如下,可以看到when().thenReturn()确实调用了CityServiceImpl的方法。而且中日志还可以看到在when().thenReturn()就直接调用了CityServiceImpl的方法,并没有等到cityRestController.findOneCity()来触发。至于测试报错,则是有CityServiceImplcityDao没有初始化导致的,这就是另外1个问题。

image-20211130224336976.png

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方法,说明类的设计可能不够合理。

猜你喜欢

转载自juejin.im/post/7036380453011456007