中发现大家都知道单元测试,也知道 junit,但是没有人知道怎么写 junit 单元测试,在这里分享我在工作中是怎么写单元测试的,供大家参考

什么是单元测试

首先讲讲什么是单元测试,单元测试是指对软件中的最小可测试单元进行检查和验证。单元测试在质量保证中是非常重要的环节,根据测试金字塔模型,越往上层的测试,所需的测试投入比例越大,效果也越差,而单元测试的成本要小的多,也更容易发现问题

单元测试 redisTemplate null 单元测试怎么写_单元测试

单元测试的过程

单元测试 redisTemplate null 单元测试怎么写_单元测试_02

完整的单元测试包括上面几个过程

数据准备

某些方法需要数据库初始化一些数据才能正常执行(如获取公众号配置信息,公众号配置信息在项目初始化的时候插如到数据库中的),在执行单元测试时,经常遇到由于所依赖的数据不存在或被修改了,或者在新的环境下,所依赖的数据库不存在,进而导致单元测试不通过

针对数据不存在/修改的情况,需要在执行测试用例前,初始化需要依赖到的一些数据,一般是数据库数据。而对于数据库不存在的情况,则使用H2内存数据库来模拟mysql环境来解决

导入H2依赖

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>${h2.version}</version>
    <scope>test</scope>
</dependency>

/src/test/resources/application.yml 配置H2数据库

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:mmall;MODE=MySQL
    # 表结构初始化脚本,多个用逗号分割
    schema: classpath:db/user_schemas.sql

/src/test/resources/user_schemas.sql

DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(50) NOT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

单元测试代码

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    // 使用Transaction清理数据
    @Transaction
    public void selectByIdTest(){
        // 数据准备
        User user = new User();
        user.setId(1);
        user.setUsername("张三");
        user.setPassword("123");
        userService.save(user);
        
        // 执行测试+结果验证
        assertNotNull(userService.selectById(1));
    }

}

有时候H2并不能完全模拟mysql,因为某些mysql特性/函数在H2中并没有,就得专门搭一个测试用的mysql来跑单元测试,为避免这种情况导致误诊,有条件的情况下建议搭一个测试用mysql

参数构造

构造调用被测方法需要传入的参数

执行测试

这一步比较简单,即执行被测方法

验证结果

验证结果返回值的正确性,统一使用junit4.4提供的assertThat断言语法,不再使用之前的assertion语句(如assertEqualsassertNotSameassertTrueassertNotNull等),assertThat语法如下

assertThat( [value], [matcher statement] );

相比于assertion,使用assertThat语法有以下优点

  1. 代码风格统一
    assertThat可以替代所有的assertion语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护
  2. 支持强大的Matcher匹配符
    Matcher匹配符具有很强的易读性,使用起来更加灵活,如:想判断某个字符串 s 是否含有子字符串"developer""Works"中间的一个
// JUnit 4.4 以前的版本:
assertTrue(s.indexOf("developer")>-1||s.indexOf("Works")>-1 );
// JUnit 4.4:
assertThat(s, anyOf(containsString("developer"), containsString("Works")));
  1. 更加易懂的日志信息
    相比assertion语句,asserThat提供的错误信息更加易懂,便于排查问题,同样是判空断言不通过,assertion
assertNotNull(userMapper.selectById(3));

单元测试 redisTemplate null 单元测试怎么写_数据_03

从报错信息只能看出是AssertionError,无法知道执行结果与期望值

assertThat(userMapper.selectById(3),notNullValue());

单元测试 redisTemplate null 单元测试怎么写_单元测试_04

不仅说明了错误类型是AssertionError,而且还打印出了期望值和执行结果,更容易排查问题

数据清理

清理第一步准备的数据,有可能影响到下一步的测试

对于支持回滚的数据库(如mysql),可使用@Transactional注解回滚数据,对于不支持回滚操作的(如redis)则需要在测试方法的最后手动清理

使用Mock框架进行测试

很多情况下,尤其在微服务架构下,被测方法往往会调用一些第三方服务,这时候当依赖的第三方服务不稳定,就会导致单元测试执行失败。这时候就需要对依赖的第三方服务进行mock,使其返回正确的结果

class UserServiceImpl implements UserService {

    private final AudienceClient audienceClient;
    
    public UserServiceImpl(AudienceClient audienceClient){
        this.audienceClient = audienceClient;
    }
    
    public User get(Long id) {
        AudienceModel audienceModel = AudienceClient.getById(1);
        // 省略...
        
        return user;
    }
}
@SpringBootTest
@RunWith(SpringRunner.class)
class UserServiceImplTest {

    @InjectMocks // 注入 UserServiceImpl bean
    private UserServiceImpl userService;
    @Mock // mock掉AudienceClient
    private AudienceClient audienceClient;

    @Test
    void get() {
        AudienceModel model = new AudienceModel();
        model.setId(1);
        model.setNickName("hxy");
        
        // mock 掉audienceClient 方法,使其放回预期结果
        Mockito.when(audienceClient.getById(1)).thenReturn(model);
        assertThat(userService.get(1),notNullValue());
    }
}

项目代码一般分为 controller、service、dao 三层。在测试 controller 的时候,由于 controller 会调用 service,service 又会调用 dao 层。当我们测试 controller 的时候,往往由于 service 层的报错导致 controller 测试不通过

controller 层主要测试参数校验逻辑,测试的时候并不需要真正的调用 service 层服务,可以通过 mock 框架将 service 方法 mock 掉

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserControllerTest {

    private MockMvc mvc;
    @InjectMocks
    private UserController userController;
    @Mock
    private UserService userService;

    @Before
    public void init() {
        mvc = MockMvcBuilders.standaloneSetup(userController)
                .build();
    }

    @Test
    public void get() throws Exception {

        Mockito.when(userService.get(1)).thenReturn(new User());

        mvc.perform(MockMvcRequestBuilders.get("/user/get?id=1")
                .characterEncoding("utf-8")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
            	// .andExpect() 根据需要可添加多个andExpect
                .andDo(MockMvcResultHandlers.print());
    }

}

service 层主要测试业务逻辑是否正确,当 service 层发生调用外部服务的时候,需要 mock 掉外部服务的调用代码,避免单元测试的时候调用外部服务,导致外部服务异常。同时单元测试不应该依赖外部服务

@SpringBootTest
@RunWith(SpringRunner.class)
class UserServiceImplTest {

    @InjectMocks
    private UserServiceImpl userService;
    @Mock
    private AudienceClient audienceClient;

    @Test
    void get() {
        Mockito.when(audienceClient.selectById(1)).thenReturn(new User());
        assertThat(userService.get(1),notNullValue());
    }
}

service 层调用 dao 层,无需 mock 掉 dao层,因为使用 mybatis-plus 有很多代码是写在 service 层的,dao 层无法单独抽出来测试,所以可以将 service 层跟 dao 层合并进行测试