7c5b494cc491cf5842434e88d12f00ec.png

单元测试

在日常的开发工作中,我们总是要经历一系列流程,大致可以总结成这样的一个流程:前期需求梳理,架构设计,编码,测试,项目正式上线等。然而,测试在整个流程中处于一个非常重要的环节,它直接关系到项目是否成功,是否满足干系人的需求,是否能够达到预期的商业价值。

测试在整个软件行业又可以分为很多种测试:

  • 单元测试(Unit Testing)
  • 集成测试(Integration Testing)
  • 系统测试(System Testing)
  • 验收测试(Acceptance Testing)

单元测试是最基础也是最重要的一种测试,它主要在程序员编码完成后用于检验程序的逻辑是否符合预期的效果,通过单元测试我们很容易判断我们的程序逻辑是否正确,它主要是对程序的一个一个函数进行测试。

同时,通过单元测试也可以检验我们编写的程序结构是否合理,可读性是否强。也就是当别人拿到我们的这一份代码的时候,是否可以很容易读懂程序想要实现的功能,以及实现该功能的逻辑是否清晰,条理是否明确。当我们的单元测试没办法编写或者很难编写的时候,这个时候我们就需要对我们编写的程序进行重构了,重构的过程就是将程序代码进行解耦,使整个程序代码的设计条理清晰,增强代码的逻辑可读性。

编写单元测试的时候,还有一种情况我们可能也需要考虑到,也可能不止下面两点:

  • 跟数据库有关的单元测试如何编写,我们不希望在数据库中写入很多没用的数据;
  • 跟外部接口有关的单元测试如何编写,我们希望当外部接口不可用的时候,我们的程序逻辑还能够符合我们预期的效果;

这个时候我们可能需要考虑一种技术就是Mock,百度百科对它是这样说的:

mock测试对象

这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。

mock测试对象使用范畴

真实对象具有不可确定的行为,产生不可预测的效果,(如:股票行情,天气预报)真实对象很难被创建的 真实对象的某些行为很难被触发真实对象实际上还不存在的(和其他开发小组或者和新的硬件打交道)等等。

也就是我们可以把单元测试中的操作数据库函数,以及发送外部请求的函数进行Mock掉,通过Mock这些操作返回几种我们预期的结果,然后进行后续程序的逻辑校验。通过这些假设的结果来验证我们程序功能是否完善。


10e75317ed1ac0e5f62dd39f2ac957d6.png

单元测试

下面我们通过一个简单的demo来看看如何使用Mock技术来编写我们的单元测试。demo我在文章底部附上分享地址。

新建SpringBoot工程

项目就叫junit-demo,其中maven的依赖如下:

<?xml version="1.0" encoding="UTF-8"?>4.0.0org.springframework.boot        spring-boot-starter-parent        2.3.1.RELEASEcom.example    junit-demo    0.0.1junit-demoDemo project for Spring Boot1.83.1.01.7.4org.springframework.boot            spring-boot-starter-web        org.mybatis.spring.boot            mybatis-spring-boot-starter            2.1.3org.springframework.boot            spring-boot-devtools            runtimetrueorg.projectlombok            lombok            truecom.alibaba            druid            1.1.20mysql            mysql-connector-java            runtimeorg.powermock            powermock-module-junit4            ${powermock.version}testorg.powermock            powermock-api-mockito2            ${powermock.version}testorg.mockito            mockito-core            ${mockito.version}testorg.mockito            mockito-junit-jupiter            ${mockito.version}testcommons-io            commons-io            2.6org.springframework.boot            spring-boot-test        org.springframework            spring-test            5.2.7.RELEASEtestorg.springframework.boot                spring-boot-maven-plugin            publicaliyun nexushttp://maven.aliyun.com/nexus/content/groups/public/truepublicaliyun nexushttp://maven.aliyun.com/nexus/content/groups/public/truefalse

application.yml

# 数据源配置spring:  datasource:    driverClassName: com.mysql.cj.jdbc.Driver    type: com.alibaba.druid.pool.DruidDataSource    url: jdbc:mysql://10.0.0.50:3306/demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8    username: root    password: root# MyBatis配置mybatis:  # 搜索指定包别名  typeAliasesPackage: com.example.demo.**.domain  # 配置mapper的扫描,找到所有的mapper.xml映射文件  mapperLocations: classpath*:mybatis/**/*Mapper.xml  # 加载全局的配置文件  configLocation: classpath:mybatis/mybatis-config.xml

我们通过mybatis操作数据库,并且在SpringBoot中配置数据源

mybatis mapper如下:

@Mapperpublic interface UserMapper {    public int insert(User user);    public int update(User user);    public int delete(Long id);    public List select();}
<?xml version="1.0" encoding="UTF-8" ?>        insert into user(        id,name,age,address        )values(        #{id},#{name},#{age},#{address}        )            update user        dept_id = #{deptId},user_name = #{userName},age = #{age},address = #{address},        where id = #{id}            select * from user            delete from sys_user where user_id = #{userId}    

我们定义一个写数据库的Service

@Servicepublic class UserService {    @Autowired    private UserMapper userMapper;    public String insert(User user) {        int result = userMapper.insert(user);        if (result > 0) {            return "add succes";        } else {            return "add failed";        }    }}

再定义一个通过RestTemplate发送外部请求的Service

@Servicepublic class RestService {    @Autowired    private RestTemplate restTemplate;    public String request() {        String resp = restTemplate.getForObject("http://www.baidu.com", String.class);        if (!StringUtils.isEmpty(resp)) {            return "success";        } else {            return "failed";        }    }}

2763ed59dfef2cde94d59980863cd875.png

单元测试

一切准备工作完成,下面我们看如何编写单元测试。

编写单元测试

我们编写UserService的单元测试

@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTestpublic class MockUserServiceTest {    @Autowired    @InjectMocks    private UserService userService;    @MockBean    private UserMapper userMapper;    @Before    public void setUp() {        MockitoAnnotations.initMocks(this);    }    @Test    public void insert1() {        User user = new User();        Mockito.when(userMapper.insert(Mockito.any())).thenReturn(1);        String result = userService.insert(user);        assertEquals("add succes", result);    }    @Test    public void insert2() {        User user = new User();        Mockito.when(userMapper.insert(Mockito.any())).thenReturn(0);        String result = userService.insert(user);        assertEquals("add failed", result);    }    @Test(expected = RuntimeException.class)    public void insert3() {        User user = new User();        Mockito.when(userMapper.insert(Mockito.any())).thenThrow(RuntimeException.class);        userService.insert(user);    }}

在这个单元测试中,我们Mock了三种情况,当然还可以Mock更多的情况:

  1. insert数据成功的情况;
  2. insert数据失败的情况;
  3. insert过程中出现异常的情况;

我们编写RestService的单元测试

@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTestpublic class MockRestServiceTest {    @Autowired    @InjectMocks    private RestService restService;    @MockBean    private RestTemplate restTemplate;    @Before    public void setUp() {        MockitoAnnotations.initMocks(this);    }    @Test    public void request1() {        Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn("test");        String result = restService.request();        assertEquals("success", result);    }    @Test    public void request2() {        Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn(null);        String result = restService.request();        assertEquals("failed", result);    }    @Test(expected = RuntimeException.class)    public void request3() {        Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenThrow(RuntimeException.class);        restService.request();    }    @Test    public void request4() {        Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenThrow(new RestClientException("test"));        try {            restService.request();        } catch (RuntimeException e) {            assertEquals("test", e.getMessage());        }    }}

在这个测试用例中,我们Mock了四种情况,当然还可以Mock更多的情况:

  1. 发外部请求成功获取结果的情况;
  2. 发外部请求没有获取到结果的情况;
  3. 发外部请求出现异常的情况;
  4. 发外部请求出现特定的异常信息的情况;

如果我们不使用Mock,我们的单元测试UserService可能就是这个样子:

@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest@Disabledpublic class UserServiceTest {    @Autowired    private UserService userService;    @Autowired    private UserMapper userMapper;    @Test    public void insert1() {        User user = new User();        user.setName("test");        user.setAge(1);        user.setAddress("address");        String result = userService.insert(user);        assertEquals("add succes", result);    }}

这种情况如果项目正常,数据库连接没问题的话,运行单元测试的时候,会向数据库中insert许多没用的数据,而且这种单元测试覆盖率是有限的,理论上来说它总是成功的,失败的情况我们在单元测试环节很难测出来。

不使用Mock我们的单元测试RestService可能就是这个样子:

@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest@Disabledpublic class RestServiceTest {    @Autowired    private RestService restService;    @Test    public void request1() {        String result = restService.request();        assertEquals("success", result);    }}

这个单元测试跟上面的单元测试情况一样,最大的问题是覆盖率有限,其他情况在单元测试环节很难测试出来。

我们通过命令行来运行我们的单元测试:

mvn test
794d5d23810c4e05b792a127af95608a.png

测试结果

通过以上demo我们可以看到通过Mock,我们可以Mock出很多情况,通过这些情况我们可以轻松的实现我们的单元测试的编写,验证我们的逻辑是否符合预期的效果。

demo分享地址:

https://github.com/bq-xiao/junit-demo.git

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐