写给想入门单元测试的你

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

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

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

🍱本篇收录完整代码地址: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...

相关推荐
Dola_Pan3 小时前
Linux文件IO(二)-文件操作使用详解
java·linux·服务器
wang_book3 小时前
Gitlab学习(007 gitlab项目操作)
java·运维·git·学习·spring·gitlab
蜗牛^^O^4 小时前
Docker和K8S
java·docker·kubernetes
从心归零4 小时前
sshj使用代理连接服务器
java·服务器·sshj
IT毕设梦工厂5 小时前
计算机毕业设计选题推荐-在线拍卖系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
Ylucius6 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
凡人的AI工具箱6 小时前
AI教你学Python 第11天 : 局部变量与全局变量
开发语言·人工智能·后端·python
是店小二呀6 小时前
【C++】C++ STL探索:Priority Queue与仿函数的深入解析
开发语言·c++·后端
七夜zippoe6 小时前
分布式系统实战经验
java·分布式
canonical_entropy6 小时前
金蝶云苍穹的Extension与Nop平台的Delta的区别
后端·低代码·架构