写给想入门单元测试的你

717 篇文章 18 订阅
127 篇文章 0 订阅

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

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

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

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

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

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

01_开发流程规范.png

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

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

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

比如下面这种代码

@Test
public void Test1(){
    xxxService.doSomeThing();
}

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

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

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

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

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

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

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

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

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

<?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、单元测试类注解解析

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

/*
 * 这个注解的作用是,在执行单元测试的时候,不是直接去执行里面的单元测试的方法
 * 因为那些方法执行之前,是需要做一些准备工作的,它是需要先初始化一个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、有返回值方法的测试

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

4、无返回值方法的测试

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

四、单元测试小例

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

(1)dao层的单元测试

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

@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层的单元测试

@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层的单元测试

@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)添加依赖

<dependency>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.2</version>
</dependency>

(2)添加插件

<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命令

报告生成位置

image.png

4、异常测试

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

六、附录

1、user建表语句:

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…

行动吧,在路上总比一直观望的要好,未来的你肯定会感谢现在拼搏的自己!如果想学习提升找不到资料,没人答疑解惑时,请及时加入群: 786229024,里面有各种测试开发资料和技术可以一起交流哦。

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】在这里插入图片描述
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值