• 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()
  • 相关阅读:
    [转]使用Java Mission Control进行内存分配分析
    JDE开发端安装问题(JDE初步卸载重装)
    JDE开发端安装笔记
    [转]JDE910--jas.ini参数说明
    [转]十个常见的缓存使用误区及建议
    JDE910笔记2--OMW项目建立及简单使用
    JDE910笔记1--基础介绍及配置
    [转] 编程之美--字符串移位包含的问题
    关于JDBC
    [转]何时使用委托而不使用接口(C# 编程指南)
  • 原文地址:https://www.cnblogs.com/vchar/p/15579398.html
Copyright © 2020-2023  润新知