zoukankan      html  css  js  c++  java
  • 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()
  • 相关阅读:
    luogu 1865 数论 线性素数筛法
    洛谷 2921 记忆化搜索 tarjan 基环外向树
    洛谷 1052 dp 状态压缩
    洛谷 1156 dp
    洛谷 1063 dp 区间dp
    洛谷 2409 dp 月赛题目
    洛谷1199 简单博弈 贪心
    洛谷1417 烹调方案 dp 贪心
    洛谷1387 二维dp 不是特别简略的题解 智商题
    2016 10 28考试 dp 乱搞 树状数组
  • 原文地址:https://www.cnblogs.com/vchar/p/15579398.html
Copyright © 2011-2022 走看看