![16f004d7f213019f0d9e98a157918c31.png](https://i-blog.csdnimg.cn/blog_migrate/3b924a17aca13feb8b7d0cba7a3ada5d.jpeg)
单元测试
在日常的开发工作中,我们总是要经历一系列流程,大致可以总结成这样的一个流程:前期需求梳理,架构设计,编码,测试,项目正式上线等。然而,测试在整个流程中处于一个非常重要的环节,它直接关系到项目是否成功,是否满足干系人的需求,是否能够达到预期的商业价值。
测试在整个软件行业又可以分为很多种测试:
- 单元测试(Unit Testing)
- 集成测试(Integration Testing)
- 系统测试(System Testing)
- 验收测试(Acceptance Testing)
单元测试是最基础也是最重要的一种测试,它主要在程序员编码完成后用于检验程序的逻辑是否符合预期的效果,通过单元测试我们很容易判断我们的程序逻辑是否正确,它主要是对程序的一个一个函数进行测试。
同时,通过单元测试也可以检验我们编写的程序结构是否合理,可读性是否强。也就是当别人拿到我们的这一份代码的时候,是否可以很容易读懂程序想要实现的功能,以及实现该功能的逻辑是否清晰,条理是否明确。当我们的单元测试没办法编写或者很难编写的时候,这个时候我们就需要对我们编写的程序进行重构了,重构的过程就是将程序代码进行解耦,使整个程序代码的设计条理清晰,增强代码的逻辑可读性。
编写单元测试的时候,还有一种情况我们可能也需要考虑到,也可能不止下面两点:
- 跟数据库有关的单元测试如何编写,我们不希望在数据库中写入很多没用的数据;
- 跟外部接口有关的单元测试如何编写,我们希望当外部接口不可用的时候,我们的程序逻辑还能够符合我们预期的效果;
这个时候我们可能需要考虑一种技术就是Mock,百度百科对它是这样说的:
mock测试对象
这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。
mock测试对象使用范畴
真实对象具有不可确定的行为,产生不可预测的效果,(如:股票行情,天气预报)真实对象很难被创建的 真实对象的某些行为很难被触发真实对象实际上还不存在的(和其他开发小组或者和新的硬件打交道)等等。
也就是我们可以把单元测试中的操作数据库函数,以及发送外部请求的函数进行Mock掉,通过Mock这些操作返回几种我们预期的结果,然后进行后续程序的逻辑校验。通过这些假设的结果来验证我们程序功能是否完善。
![c92ec1a77cfb49aa7d68e77ed4064d76.png](https://i-blog.csdnimg.cn/blog_migrate/c26c502468601a7429f8039bb8f357ea.jpeg)
单元测试
下面我们通过一个简单的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"; } }}
![160bd7c2d33b969960bcb8036716aab6.png](https://i-blog.csdnimg.cn/blog_migrate/edb0b98d9228748b12b9aab4df773f2f.jpeg)
单元测试
一切准备工作完成,下面我们看如何编写单元测试。
编写单元测试
我们编写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更多的情况:
- insert数据成功的情况;
- insert数据失败的情况;
- 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更多的情况:
- 发外部请求成功获取结果的情况;
- 发外部请求没有获取到结果的情况;
- 发外部请求出现异常的情况;
- 发外部请求出现特定的异常信息的情况;
如果我们不使用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
![81443c11fa5c6aa7488009587ab3b3a3.png](https://i-blog.csdnimg.cn/blog_migrate/26976484c8386b4d9d99e3a241e2e8ea.jpeg)
测试结果
通过以上demo我们可以看到通过Mock,我们可以Mock出很多情况,通过这些情况我们可以轻松的实现我们的单元测试的编写,验证我们的逻辑是否符合预期的效果。
demo分享地址:
https://github.com/bq-xiao/junit-demo.git