写给想入门单元测试的你

✨这里是第七人格的博客✨小七,欢迎您的到来~✨

🍅系列专栏:【架构思想】🍅

✈️本篇内容: 写给想入门单元测试的你✈️

🍱本篇收录完整代码地址:gitee.com/diqirenge/s...🍱

一、为什么要进行单元测试

首先我们来看一下标准的软件开发流程是什么样的

从图中我们可以看到,单元测试作为开发流程中的重要一环,其实是保证代码健壮性的重要一环,但是因为各种各样的原因,在日常开发中,我们往往不重视这一步,不写或者写的不太规范。那为什么要进行单元测试呢?小七觉得有以下几点:

  • 便于后期重构。单元测试可以为代码的重构提供保障,只要重构代码之后单元测试全部运行通过,那么在很大程度上表示这次重构没有引入新的BUG,当然这是建立在完整、有效的单元测试覆盖率的基础上。
  • 优化设计。编写单元测试将使用户从调用者的角度观察、思考,特别是使用TDD驱动开发的开发方式,会让使用者把程序设计成易于调用和可测试,并且解除软件中的耦合。
  • 文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
  • 具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。

不少同学,写单元测试,就是直接调用的接口方法,就跟跑swagger和postMan一样,这样只是对当前方法有无错误做了一个验证,无法构成单元测试网络。

比如下面这种代码

java 复制代码
@Test
public void Test1(){
    xxxService.doSomeThing();
}

接下来小七就和大家探讨一下如何写好一个简单的单元测试。

小七觉得写好一个单元测试应该要注意以下几点:

1、单元测试是主要是关注测试方法的逻辑,而不仅仅是结果。

2、需要测试的方法,不应该依赖于其他的方法,也就是说每一个单元各自独立。

3、无论执行多少次,其结果是一定的不变的,也就是单元测试需要有幂等性。

4、单元测试也应该迭代维护。

二、单元测试需要引用的jar包

针对springboot项目,咱们只需要引用他的starter即可

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

下面贴出这个start包含的依赖

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starters</artifactId>
    <version>2.1.0.RELEASE</version>
  </parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <version>2.1.0.RELEASE</version>
  <name>Spring Boot Test Starter</name>
  <description>Starter for testing Spring Boot applications with libraries including
      JUnit, Hamcrest and Mockito</description>
  <url>https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-starters/spring-boot-starter-test</url>
  <organization>
    <name>Pivotal Software, Inc.</name>
    <url>https://spring.io</url>
  </organization>
  <licenses>
    <license>
      <name>Apache License, Version 2.0</name>
      <url>http://www.apache.org/licenses/LICENSE-2.0</url>
    </license>
  </licenses>
  <developers>
    <developer>
      <name>Pivotal</name>
      <email>info@pivotal.io</email>
      <organization>Pivotal Software, Inc.</organization>
      <organizationUrl>http://www.spring.io</organizationUrl>
    </developer>
  </developers>
  <scm>
    <connection>scm:git:git://github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-test</connection>
    <developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-test</developerConnection>
    <url>http://github.com/spring-projects/spring-boot/spring-boot-starters/spring-boot-starter-test</url>
  </scm>
  <issueManagement>
    <system>Github</system>
    <url>https://github.com/spring-projects/spring-boot/issues</url>
  </issueManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>2.1.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-test</artifactId>
      <version>2.1.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-test-autoconfigure</artifactId>
      <version>2.1.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.jayway.jsonpath</groupId>
      <artifactId>json-path</artifactId>
      <version>2.4.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <version>3.11.1</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>2.23.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-core</artifactId>
      <version>1.3</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-library</artifactId>
      <version>1.3</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.skyscreamer</groupId>
      <artifactId>jsonassert</artifactId>
      <version>1.5.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.1.2.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.1.2.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.xmlunit</groupId>
      <artifactId>xmlunit-core</artifactId>
      <version>2.6.2</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

三、单元测试解析与技巧

1、单元测试类注解解析

下面是出现频率极高的注解:

java 复制代码
/*
 * 这个注解的作用是,在执行单元测试的时候,不是直接去执行里面的单元测试的方法
 * 因为那些方法执行之前,是需要做一些准备工作的,它是需要先初始化一个spring容器的
 * 所以得找这个SpringRunner这个类,来先准备好spring容器,再执行各个测试方法
 */
@RunWith(SpringRunner.class) 
/*
 * 这个注解的作用是,去寻找一个标注了@SpringBootApplication注解的一个类,也就是启动类
 * 然后会执行这个启动类的main方法,就可以创建spring容器,给后面的单元测试提供完整的这个环境
 */
@SpringBootTest
/*
 * 这个注解的作用是,可以让每个方法都是放在一个事务里面
 * 让单元测试方法执行的这些增删改的操作,都是一次性的
 */
@Transactional 
/*
 * 这个注解的作用是,如果产生异常那么会回滚,保证数据库数据的纯净
 * 默认就是true
 */
@Rollback(true)

2、常用断言

Junit所有的断言都包含在 Assert 类中。

void assertEquals(boolean expected, boolean actual) 检查两个变量或者等式是否平衡
void assertTrue(boolean expected, boolean actual) 检查条件为真
void assertFalse(boolean condition) 检查条件为假
void assertNotNull(Object object) 检查对象不为空
void assertNull(Object object) 检查对象为空
void assertArrayEquals(expectedArray, resultArray) 检查两个数组是否相等
void assertSame(expected, actual) 查看两个对象的引用是否相等。类似于使用"=="比较两个对象
assertNotSame(unexpected, actual) 查看两个对象的引用是否不相等。类似于使用"!="比较两个对象
fail() 让测试失败
static T verify(T mock, VerificationMode mode) 验证调用次数,一般用于void方法

3、有返回值方法的测试

java 复制代码
@Test
public void haveReturn() {
   // 1、初始化数据
   // 2、模拟行为
   // 3、调用方法
   // 4、断言
}

4、无返回值方法的测试

java 复制代码
@Test
public void noReturn() {
   // 1、初始化数据
   // 2、模拟行为
   // 3、调用方法
   // 4、验证执行次数
}

四、单元测试小例

以常见的SpringMVC3层架构为例,咱们分别展示3层架构如何做简单的单元测试。业务场景为用户user的增删改查。

(1)dao层的单元测试

dao层一般是持久化层,也就是与数据库打交道的一层,单元测试尽量不要依赖外部,但是直到最后一层的时候,DAO层的时候,还是要依靠开发环境里的基础设施,来进行单元测试。

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback
public class UserMapperTest {

    /**
     * 持久层,不需要使用模拟对象
     */
    @Autowired
    private UserMapper userMapper;

    /**
     * 测试用例:查询所有用户信息
     */
    @Test
    public void testListUsers() {
        // 初始化数据
        initUser(20);
        // 调用方法
        List<User> resultUsers = userMapper.listUsers();
        // 断言不为空
        assertNotNull(resultUsers);
        // 断言size大于0
        Assert.assertThat(resultUsers.size(), is(greaterThanOrEqualTo(0)));
    }

    /**
     * 测试用例:根据ID查询一个用户
     */
    @Test
    public void testGetUserById() {
        // 初始化数据
        User user = initUser(20);
        Long userId = user.getId();
        // 调用方法
        User resultUser = userMapper.getUserById(userId);
        // 断言对象相等
        assertEquals(user.toString(), resultUser.toString());
    }

    /**
     * 测试用例:新增用户
     */
    @Test
    public void testSaveUser() {
        initUser(20);
    }

    /**
     * 测试用例:修改用户
     */
    @Test
    public void testUpdateUser() {
        // 初始化数据
        Integer oldAge = 20;
        Integer newAge = 21;
        User user = initUser(oldAge);
        user.setAge(newAge);
        // 调用方法
        Boolean updateResult = userMapper.updateUser(user);
        // 断言是否为真
        assertTrue(updateResult);
        // 调用方法
        User updatedUser = userMapper.getUserById(user.getId());
        // 断言是否相等
        assertEquals(newAge, updatedUser.getAge());
    }

    /**
     * 测试用例:删除用户
     */
    @Test
    public void testRemoveUser() {
        // 初始化数据
        User user = initUser(20);
        // 调用方法
        Boolean removeResult = userMapper.removeUser(user.getId());
        // 断言是否为真
        assertTrue(removeResult);
    }

    private User initUser(int i) {
        // 初始化数据
        User user = new User();
        user.setName("测试用户");
        user.setAge(i);
        // 调用方法
        userMapper.saveUser(user);
        // 断言id不为空
        assertNotNull(user.getId());
        return user;
    }
}

(2)service层的单元测试

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {

   @Autowired
   private UserService userService;

   /**
    * 这个注解表名,该对象是个mock对象,他将替换掉你@Autowired标记的对象
    */
   @MockBean
   private UserMapper userMapper;
   
   /**
    * 测试用例:查询所有用户信息
    */
   @Test
   public void testListUsers() {
      // 初始化数据
      List<User> users = new ArrayList<>();

      User user = initUser(1L);
      
      users.add(user);
      // mock行为
      when(userMapper.listUsers()).thenReturn(users);
      // 调用方法
      List<User> resultUsers = userService.listUsers();
      // 断言是否相等
      assertEquals(users, resultUsers);
   }
   
   /**
    * 测试用例:根据ID查询一个用户
    */
   @Test
   public void testGetUserById() {
      // 初始化数据
      Long userId = 1L;

      User user = initUser(userId);
      // mock行为
      when(userMapper.getUserById(userId)).thenReturn(user);
      // 调用方法
      User resultUser = userService.getUserById(userId);
      // 断言是否相等
      assertEquals(user, resultUser);

   }
   
   /**
    * 测试用例:新增用户
    */
   @Test
   public void testSaveUser() {
      // 初始化数据
      User user = initUser(1L);
      // 默认的行为(这一行可以不写)
      doNothing().when(userMapper).saveUser(any());
      // 调用方法
      userService.saveUser(user);
      // 验证执行次数
      verify(userMapper, times(1)).saveUser(user);

   }

   /**
    * 测试用例:修改用户
    */
   @Test
   public void testUpdateUser() {
      // 初始化数据
      User user = initUser(1L);
      // 模拟行为
      when(userMapper.updateUser(user)).thenReturn(true);
      // 调用方法
      Boolean updateResult = userService.updateUser(user);
      // 断言是否为真
      assertTrue(updateResult); 
   }

   /**
    * 测试用例:删除用户
    */
   @Test
   public void testRemoveUser() {
      Long userId = 1L;
      // 模拟行为
      when(userMapper.removeUser(userId)).thenReturn(true);
      // 调用方法
      Boolean removeResult = userService.removeUser(userId);
      // 断言是否为真
      assertTrue(removeResult);
   }

   private User initUser(Long userId) {
      User user = new User();
      user.setName("测试用户");
      user.setAge(20);
      user.setId(userId);
      return user;
   }
   
}

(3)controller层的单元测试

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class UserControllerTest {

   private MockMvc mockMvc;

   @InjectMocks
   private UserController userController;

   @MockBean
   private UserService userService;

   /**
    * 前置方法,一般执行初始化代码
    */
   @Before
   public void setup() {

      MockitoAnnotations.initMocks(this);

      this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
   }
   
   /**
    * 测试用例:查询所有用户信息
    */
   @Test
   public void testListUsers() {
      try {
         List<User> users = new ArrayList<User>();
         
         User user = new User();
         user.setId(1L);
         user.setName("测试用户");  
         user.setAge(20);
         
         users.add(user);
         
         when(userService.listUsers()).thenReturn(users);
         
         mockMvc.perform(get("/user/"))
               .andExpect(content().json(JSONArray.toJSONString(users))); 
      } catch (Exception e) {
         e.printStackTrace(); 
      }
   }
   
   /**
    * 测试用例:根据ID查询一个用户
    */
   @Test
   public void testGetUserById() {
      try {
         Long userId = 1L;
         
         User user = new User();
         user.setId(userId);
         user.setName("测试用户");  
         user.setAge(20);
         
         when(userService.getUserById(userId)).thenReturn(user);
         
         mockMvc.perform(get("/user/{id}", userId))  
               .andExpect(content().json(JSONObject.toJSONString(user)));
      } catch (Exception e) {
         e.printStackTrace(); 
      }
   }
   
   /**
    * 测试用例:新增用户
    */
   @Test
   public void testSaveUser() {
      Long userId = 1L;
      
      User user = new User();
      user.setName("测试用户");  
      user.setAge(20);
      
      when(userService.saveUser(user)).thenReturn(userId);
      
      try {
         mockMvc.perform(post("/user/").contentType("application/json").content(JSONObject.toJSONString(user)))
               .andExpect(content().string("success"));
      } catch (Exception e) {
         e.printStackTrace(); 
      }
   }
   
   /**
    * 测试用例:修改用户
    */
   @Test
   public void testUpdateUser() {
      Long userId = 1L;
      
      User user = new User();
      user.setId(userId); 
      user.setName("测试用户");  
      user.setAge(20);
      
      when(userService.updateUser(user)).thenReturn(true);
      
      try {
         mockMvc.perform(put("/user/{id}", userId).contentType("application/json").content(JSONObject.toJSONString(user)))  
               .andExpect(content().string("success"));     
      } catch (Exception e) {
         e.printStackTrace(); 
      }
   }
   
   /**
    * 测试用例:删除用户
    */
   @Test
   public void testRemoveUser() {
      Long userId = 1L;

      when(userService.removeUser(userId)).thenReturn(true);  
      
      try {
         mockMvc.perform(delete("/user/{id}", userId))   
               .andExpect(content().string("success"));     
      } catch (Exception e) {
         e.printStackTrace(); 
      }
   }
   
}

五、其他

1、小七认为不需要对私有方法进行单元测试。

2、dubbo的接口,在初始化的时候会被dubbo的类代理,和单测的mock是两个类,会导致mock失效,目前还没有找到好的解决方案。

3、单元测试覆盖率报告

(1)添加依赖

xml 复制代码
<dependency>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.2</version>
</dependency>

(2)添加插件

xml 复制代码
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.2</version>
    <executions>
        <execution>
            <id>pre-test</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>post-test</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

(3)执行mvn test命令

报告生成位置

4、异常测试

本次分享主要是针对正向流程,异常情况未做处理。感兴趣的同学可以查看附录相关文档自己学习。

六、附录

1、user建表语句:

sql 复制代码
CREATE TABLE `user` (
  `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
  `name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名',
  `age` INT(3) NOT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='user示例表';

2、文章小例源码地址:gitee.com/diqirenge/s...

3、mockito官网:site.mockito.org/

4、mockito中文文档:github.com/hehonghui/m...

相关推荐
Chrikk2 分钟前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*5 分钟前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue5 分钟前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man8 分钟前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟9 分钟前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity1 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天1 小时前
java的threadlocal为何内存泄漏
java
caridle1 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^1 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋32 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx