Java单元测试实战


在一个项目开发中我们通常都是分工合作共同开发的,那么在业务中各个模块可能会存在相互调用的情况。如果我们调用的某个模块开发的同学还未开发完成,那么在进行单元测试的时候该如何办呢?或者是我们只是想测试某个业务的逻辑代码,不需要去连接那些基础组件(比如数据库这些)时,又应该如何做呢?再比如我们只想测试在某种情况下会自己的逻辑代码是否正确,此时又该如何做呢?

当然你可能会想到直接去将相关的代码写死即可,但是万一改动的地方比较多就很麻烦了;同时有的地方你改为死数据时,很可能待会儿你提交代码时就会忘记,最后可能就会直接发布到正式环境里面去了。虽然直接写死的方式效率很快,但是也容易发生错误;因此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()

特别提醒:扫码关注微信订阅号'起岸星辰',实时掌握IT业界技术资讯! 转载请保留原文中的链接!
  目录