在一个项目开发中我们通常都是分工合作共同开发的,那么在业务中各个模块可能会存在相互调用的情况。如果我们调用的某个模块开发的同学还未开发完成,那么在进行单元测试的时候该如何办呢?或者是我们只是想测试某个业务的逻辑代码,不需要去连接那些基础组件(比如数据库这些)时,又应该如何做呢?再比如我们只想测试在某种情况下会自己的逻辑代码是否正确,此时又该如何做呢?
当然你可能会想到直接去将相关的代码写死即可,但是万一改动的地方比较多就很麻烦了;同时有的地方你改为死数据时,很可能待会儿你提交代码时就会忘记,最后可能就会直接发布到正式环境里面去了。虽然直接写死的方式效率很快,但是也容易发生错误;因此mock就是用来解决这些问题的,将mock和单元测试搭配后我们就可以轻松进行各个模块的测试工作了(当然也会花费更多的步骤)。下面我们将通过一些示例来带你快速了解如何在单元测试中使用mock。
注意:如果你公司的业务需求变更非常快,那么不建议写单元测试,因为可能你的单测还没写完,需求就已经发生变更了。
依赖的包说明
因为我们目前的开发基本都是基于spring-boot来的。因此需要添加相关的依赖包:
<!--单元测试需要的包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!--要进行静态方法mock时需要引入powermock的依赖包-->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
一些常用测试注解说明
@AutoConfigureMockMvc
该注解表示 MockMvc由spring容器构建,你只负责注入之后用就可以了。这种写法是为了让测试在Spring容器环境下执行。@Mock
会虚拟一个对象,在使用它创建对象的方法时将不会真正执行,如果没有写when().thenReturn()
语句时将直接返回null;即可以理解为没有匹配的when().thenReturn()
时都会返回null。同时这个注解相当于:Mockito.mock(xxx.class)
来手动创建@Spy
这和@Mock
的区别是,它会实际的执行代码逻辑。@InjectMocks
这个会自己将注入类中相关依赖的对自动模拟
单元测试示例
下面我们将举例常用的场景单元测试的示例。示例中使用的是 spring-boot:2.5.6
。
测试路由层Controller
测试路由层主要就是一个模拟的请求,这样便于我们在以后更新维护后,可以方便进行回归测试。
示例1:测试路由层Controller,使用mock模拟请求
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestDemoApplication.class)
@AutoConfigureMockMvc
public class MockMvcTest {
@Autowired
private MockMvc mockMvc;
@Test
public void apiGETest() throws Exception {
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/hotel/detail?id=1"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
mvcResult.getResponse().setCharacterEncoding("UTF-8");
System.out.println(mvcResult.getResponse().getContentAsString());
}
}
示例2:测试路由层Controller,使用mock模拟业务逻辑service层
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestDemoApplication.class)
@AutoConfigureMockMvc
public class HotelControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private HotelService hotelService;
@Test
public void findHotel() throws Exception {
Hotel hotel = new Hotel();
hotel.setId(1);
hotel.setHotelName("世外桃源酒店");
hotel.setRoomNum(20);
hotel.setPrice(new BigDecimal("120.90"));
BDDMockito.given(this.hotelService.findById(ArgumentMatchers.anyInt())).willReturn(hotel);
// 这里是模拟发起一个http请求
mockMvc.perform(MockMvcRequestBuilders.get("/hotel/detail?id=1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON))
// 对结果断言
.andExpect(MockMvcResultMatchers.jsonPath("$.roomNum").value(20))
// 打印请求内容
.andDo(MockMvcResultHandlers.print());
}
}
测试业务层代码service
通常我们在service中编写逻辑实现,因此在进行单元测试的时候,需要考虑如下的条件:
- 测试之前自动构造好数据,测试结束之后自动回滚数据构造
- 将service依赖的service进行模拟打桩进来
- 可能需要在数据库中构造好数据
示例3:只执行service中的代码逻辑,数据库的查询直接使用模拟的操作
@RunWith(MockitoJUnitRunner.class)
public class HotelServiceTest {
@Spy
@InjectMocks
private HotelService hotelService = new HotelServiceImpl();
@Mock
private HotelMapper hotelMapper;
@Before
public void before(){
Mockito.when(hotelMapper.addHotel(Mockito.any())).thenReturn(1);
}
@Test
public void addHotel(){
Hotel hotel = new Hotel();
hotel.setId(5);
hotel.setHotelName("云山大酒店");
hotel.setPrice(new BigDecimal("230"));
hotel.setRoomNum(130);
hotelService.addHotel(hotel);
}
}
示例4:测试业务层代码service,基于spring boot 测试框架进行单元测试(运行速度较慢)
这里通过测试验证方法是否被调用,调用顺序是否正确
@Rollback
@Transactional
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestDemoApplication.class)
public class HotelServiceTest2 {
@MockBean
private AccountService accountService;
@Autowired
private HotelService hotelService;
@Test
public void joinHotel(){
Hotel hotel = new Hotel();
hotel.setHotelName("大酒店");
hotel.setPrice(new BigDecimal("230"));
hotel.setRoomNum(130);
// 设置任意参数均返回固定参数
BDDMockito.given(this.accountService.findAccount(ArgumentMatchers.anyInt())).willReturn(null);
Integer id = hotelService.joinHotel(hotel);
Assert.assertNotNull(id);
// 接口被调用测试统计
Mockito.verify(accountService, Mockito.times(1))
.findAccount(ArgumentMatchers.anyInt());
// 检查执行方法执行顺序是否正确;inOrder(accountService)的参数可以多个,参数必须为mock出来的对象
InOrder inOrder = Mockito.inOrder(accountService);
inOrder.verify(accountService).findAccount(ArgumentMatchers.anyInt());
inOrder.verify(accountService).addAccount(ArgumentMatchers.anyInt(), ArgumentMatchers.anyString());
}
}
测试数据层Mapper
这个通常用的比较少,一般是复杂SQL语句时,为了快速的验证是否正确,才会编写的。
示例5:测试数据层Mapper,快速验证编写的SQL语句是否正确
@Rollback
@Transactional(rollbackFor = Exception.class)
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestDemoApplication.class)
public class HotelMapperTest {
@Autowired
private HotelMapper hotelMapper;
/**
* 对于@Sql 注解说明:可以提前执行一些SQL,比如下面要验证SQL需要的数据
*/
@Sql("/hotel.sql")
@Test
public void findHotel(){
Hotel hotel = hotelMapper.selectById(100);
System.out.println(hotel);
}
}
静态方法测试
当需要测试的方法中调用了静态方法,但是我们不想让静态方法执行,此时需要使用powermock来对静态方法进行mock。
需要将测试框架切换为@RunWith(PowerMockRunner.class)
,同时在@PrepareForTest
注解中指定需要mock的静态方法所属的类;其他的操作都和原来的一样。
示例6:静态方法mock
@PrepareForTest({CacheUtil.class})// 可以配置多个,如果里面还依赖了其他静态类,也需要这这里配置上
@RunWith(PowerMockRunner.class)
public class StaticMethodTest {
@Spy
@InjectMocks
private final HotelService hotelService = new HotelServiceImpl();
// @Mock
// private HotelMapper hotelMapper;
@Test
public void findHotel(){
Hotel hotel = new Hotel();
hotel.setId(1);
hotel.setHotelName("大酒店");
hotel.setPrice(new BigDecimal("230"));
hotel.setRoomNum(130);
// Mockito.when(hotelMapper.selectById(1)).thenReturn(hotel);
// 对CacheUtil的静态方法着mock
PowerMockito.mockStatic(CacheUtil.class);
PowerMockito.when(CacheUtil.getVal(Mockito.any())).thenReturn(hotel);
Hotel val = hotelService.findById(1);
Assert.assertNotNull(val);
}
}
其他注解和断言语句
assertThat和Hamcrest
- 单元测试结构
- @Before
- @Test
- @After
- 断言
- assertEquals
- assertTrue / assertFalse
- assertNull / assertNotNull
- assertSame / assertNotSame
- assertArrayEquals
- assertThat
- 测试异常
- @Test(expected = NullPointException.class)
- 主动失败
- fail
- JUnit + Hamcrest
- assertThat(str.indexOf("hello"), is(not(-1)))
- assertThat(str.contains("hello"), equals(true))
- assertThat(str, containsString("hello"))
- is、not
- equalTo / sameInstance、nullValue / notNullValue、instanceOf
- hasProperty
- hasEntry、hasKey、hasValue、hasItem / hasItems、hasItemInArray、in
- greaterThan、greaterThanOrEqualTo、lessThan、lessThanOrEqualTo
- containsString、endsWith、startsWith
- JUnit + Mockito
- when().thenReturn()