Java单元测试浅析(JUnit+Mockito)

发表于:2023-5-29 09:56

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:秦彪    来源:知乎

  1. 什么是单元测试
  (1)单元测试环节:
  测试过程按照阶段划分分为:单元测试、集成测试、系统测试、验收测试等。相关含义如下:
  1) 单元测试: 针对计算机程序模块进行输出正确性检验工作。
  2) 集成测试: 在单元测试基础上,整合各个模块组成子系统,进行集成测试。
  3) 系统测试: 将整个交付所涉及的协作内容都纳入其中考虑,包含计算机硬件、软件、接口、操作等等一系列作为一个整体,检验是否满足软件或需求说明。
  4) 验收测试: 在交付或者发布之前对所做的工作进行测试检验。
  单元测试是阶段性测试的首要环节,也是白盒测试的一种,该内容的编写与实践可以前置在研发完成,研发在编写业务代码的时候就需要生成对应代码的单元测试。单元测试的发起人是程序设计者,受益人也是编写程序的人,所以对于程序员,非常有必要形成自我约束力,完成基本的单元测试用例编写。
  (2)单元测试特征:
  由上可知,单元测试其实是针对软件中最小的测试单元来进行验证的。这里的单元就是指相关的功能子集,比如一个方法、一个类等。值得注意的是作为最低级别的测试活动,单元测试验证的对象仅限于当前测试内容,与程序其它部分内容相隔离,总结起来单元测试有以下特征:
  1) 主要功能是证明编写的代码内容与期望输出一致。
  2) 最小最低级的测试内容,由程序员自身发起,保证程序基本组件正常。
  3) 单元测试尽量不要区分类与方法,主张以过程性的方法为测试单位,简单实用高效为目标。
  4) 不要偏离主题,专注于测试一小块的代码,保证基础功能。
  5) 剥离与外部接口、存储之间的依赖,使单元测试可控。
  6) 任何时间任何顺序执行单元测试都需要是成功的。
  2. 为什么要单元测试
  (1)单元测试意义:
  程序代码都是由基本单元不断组合成复杂的系统,底层基本单元都无法保证输入输出正确性,层级递增时,问题就会不断放大,直到整个系统崩溃无法使用。所以单元测试的意义就在于保证基本功能是正常可用且稳定的。而对于接口、数据源等原因造成的不稳定因素,是外在原因,不在单元测试考虑范围之内。
  (2)使用 main 方法进行测试:
  @PostMapping(value="/save")
  public Map<String,Object> save(@RequestBody Student stu) {
      studentService.save(stu);
      Map<String,Object> params = new HashMap<>();
      params.put("code",200);
      params.put("message","保存成功");
      return params;
  }
  假如要对上面的 Controller 进行测试,可以编写如下的代码示例,使用 main 方法进行测试的时候,先启动整个工程应用,然后编写 main 方法如下进行访问,在单步调试代码。
  public static void main(String[] args) {
          HttpHeaders headers = new HttpHeaders();
          headers.setContentType(MediaType.APPLICATION_JSON);
          String json = "{"name":"张三","className":"三年级一班","age":"20","sex":"男"}";
          HttpEntity<String> httpEntity = new HttpEntity<>(json, headers);
          String url = "http://localhost:9092/student/save";
          MainMethodTest test = new MainMethodTest();
          ResponseEntity<Map> responseEntity = test.getRestTemplate().postForEntity(url, httpEntity, Map.class);
          System.out.println(responseEntity.getBody());
      }
  (3)使用 main 方法进行测试的缺点:
  1) 通过编写大量的 main 方法针对每个内容做打印输出到控制台枯燥繁琐,不具备优雅性。
  2) 测试方法不能一起运行,结果需要程序员自己判断正确性。
  3) 统一且重复性工作应该交给工具去完成。
  3. 单元测试框架 - JUnit
  3.1 JUnit 简介
  JUnit 官网:https://junit.org/。JUnit 是一个用于编写可重复测试的简单框架。它是用于单元测试框架的 xUnit 体系结构的一个实例。
  JUnit 的特点:
  (1) 针对于 Java 语言特定设计的单元测试框架,使用非常广泛。
  (2) 特定领域的标准测试框架。
  (3) 能够在多种 IDE 开发平台使用,包含 Idea、Eclipse 中进行集成。
  (4) 能够方便由 Maven 引入使用。
  (5) 可以方便的编写单元测试代码,查看测试结果等。
  JUnit 的重要概念:
  JUnit 的一些注意事项及规范:
  (1) 测试方法必须使用 @Test 修饰
  (2) 测试方法必须使用 public void 进行修饰,不能带参数
  (3) 测试代码的包应该和被测试代码包结构保持一致
  (4) 测试单元中的每个方法必须可以独立测试,方法间不能有任何依赖
  (5) 测试类一般使用 Test 作为类名的后缀
  (6) 测试方法使一般用 test 作为方法名的前缀
  JUnit 失败结果说明:
  (1) Failure:测试结果和预期结果不一致导致,表示测试不通过
  (2) error:由异常代码引起,它可以产生于测试代码本身的错误,也可以是被测代码的 Bug
  3.2 JUnit 内容
  (1) 断言的 API
  (2) JUnit 常用注解:
  1) @Test: 定义一个测试方法 @Test (excepted=xx.class): xx.class 表示异常类,表示测试的方法抛出此异常时,认为是正常的测试通过的 @Test (timeout = 毫秒数) : 测试方法执行时间是否符合预期。
  2) @BeforeClass: 在所有的方法执行前被执行,static 方法全局只会执行一次,而且第一个运行。
  3) @AfterClass:在所有的方法执行之后进行执行,static 方法全局只会执行一次,最后一个运行。
  4) @Before:在每一个测试方法被运行前执行一次。
  5) @After:在每一个测试方法运行后被执行一次。
  6) @Ignore:所修饰的测试方法会被测试运行器忽略。
  7) @RunWith:可以更改测试执行器使用 junit 测试执行器。
  3.3 JUnit 使用
  3.3.1 Controller 层单元测试
  (1) Springboot 中使用 maven 引入 Junit 非常简单,使用如下依赖即可引入:
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
  </dependency>
  (2) 上面使用 main 方法案例可以使用如下的 Junit 代码完成:
  @RunWith(SpringRunner.class)
  @SpringBootTest(classes = MainApplication.class)
  public class StudentControllerTest {
  
  // 注入Spring容器
      @Autowired
      private WebApplicationContext applicationContext;
      // 模拟Http请求
      private MockMvc mockMvc;
      @Before
      public void setupMockMvc(){
      // 初始化MockMvc对象
          mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
      }
      
      /**
       * 新增学生测试用例
       * @throws Exception
       */
      @Test
      public void addStudent() throws Exception{
          String json="{"name":"张三","className":"三年级一班","age":"20","sex":"男"}";
          mockMvc.perform(MockMvcRequestBuilders.post("/student/save")    //构造一个post请求
                      // 发送端和接收端数据格式
                      .contentType(MediaType.APPLICATION_JSON_UTF8)
                      .accept(MediaType.APPLICATION_JSON_UTF8)
                      .content(json.getBytes())
              )
             // 断言校验返回的code编码
             .andExpect(MockMvcResultMatchers.status().isOk())
             // 添加处理器打印返回结果
             .andDo(MockMvcResultHandlers.print());
      }
  }
  只需要在类或者指定方法上右键执行即可,可以直接充当 postman 工作访问指定 url,且不需要写请求代码,这些都由工具自动完成。
  (3)案例中相关组件介绍
  本案例中构造 mockMVC 对象时,也可以使用如下方式:
  @Autowired
  private StudentController studentController;
  @Before
  public void setupMockMvc(){
     // 初始化MockMvc对象
     mockMvc = MockMvcBuilders.standaloneSetup(studentController).build();
  }
  其中 MockMVC 是 Spring 测试框架提供的用于 REST 请求的工具,是对 Http 请求的模拟,无需启动整个模块就可以对 Controller 层进行调用,速度快且不依赖网络环境。
  使用 MockMVC 的基本步骤如下:
  ·mockMvc.perform 执行请求
  · MockMvcRequestBuilders.post 或 get 构造请求
  · MockHttpServletRequestBuilder.param 或 content 添加请求参数
  · MockMvcRequestBuilders.contentType 添加请求类型
  · MockMvcRequestBuilders.accept 添加响应类型
  · ResultActions.andExpect 添加结果断言
  · ResultActions.andDo 添加返回结果后置处理
  · ResultActions.andReturn 执行完成后返回相应结果
  3.3.2 Service 层单元测试
  可以编写如下代码对 Service 层查询方法进行单测:
  @RunWith(SpringRunner.class)
  @SpringBootTest
  public class StudentServiceTest {
  @Autowired
      private StudentService studentService;
      @Test
      public void getOne() throws Exception {
       Student stu = studentService.selectByKey(5);
           Assert.assertThat(stu.getName(),CoreMatchers.is("张三"));
      }
  }
  执行结果:
  3.3.3 Dao 层单元测试
  可以编写如下代码对 Dao 层保存方法进行单测:
  @RunWith(SpringRunner.class)
  @SpringBootTest
  public class StudentDaoTest {
  @Autowired
      private StudentMapper studentMapper;
      @Test
      @Rollback(value = true)
      @Transactional
      public void insertOne() throws Exception {
       Student student = new Student();
       student.setName("李四");
       student.setMajor("计算机学院");
       student.setAge(25);
       student.setSex('男');
       int count = studentMapper.insert(student);
       Assert.assertEquals(1, count);
      }
  }
  其中 @Rollback (value = true) 可以执行单元测试之后回滚所新增的数据,保持数据库不产生脏数据。
  3.3.4 异常测试
  (1) 在 service 层定义一个异常情况:
  public void computeScore() {
     int a = 10, b = 0;
  }
  (2) 在 service 的测试类中定义单元测试方法:
  @Test(expected = ArithmeticException.class)
      public void computeScoreTest() {
          studentService.computeScore();
      }
  (3) 执行单元测试也会通过,原因是 @Test 注解中的定义了异常。
  3.3.5 测试套件测多个类
  (1) 新建一个空的单元测试类
  (2) 利用注解 @RunWith (Suite.class) 和 @SuiteClasses 标明要一起单元测试的类
  @RunWith(Suite.class)
  @Suite.SuiteClasses({ StudentServiceTest.class, StudentDaoTest.class})
  public class AllTest {
  }
  运行结果:
  3.3.6 idea 中查看单元测试覆盖率
  (1) 单测覆盖率
  测试覆盖率是衡量测试过程工作本身的有效性,提升测试效率和减少程序 bug,提升产品可靠性与稳定性的指标。
  统计单元测试覆盖率的意义:
  1) 可以洞察整个代码中的基础组件功能的所有盲点,发现相关问题。
  2) 提高代码质量,通常覆盖率低表示代码质量也不会太高,因为单测不通过本来就映射出考虑到各种情况不够充分。
  3) 从覆盖率的达标上可以提高代码的设计能力。
  (2) 在 idea 中查看单元测试覆盖率很简单,只需按照图中示例的图标运行,或者在单元测试方法或类上右键 Run 'xxx' with Coverage 即可。执行结果是一个表格,列出了类、方法、行数、分支覆盖情况。
  (3) 在代码中会标识出覆盖情况,绿色的是已覆盖的,红色的是未覆盖的。
  (4) 如果想要导出单元测试的覆盖率结果,可以使用如下图所示的方式,勾选 Open generated HTML in browser
  导出结果:
  3.3.7 JUnit 插件自动生成单测代码
  (1) 安装插件,重启 idea 生效
  (2) 配置插件
  (3) 使用插件
  在需要生成单测代码的类上右键 generate...,如下图所示。
  生成结果:
  4. 单元测试工具 - Mockito
  4.1 Mockito 简介
  在单元测试过程中主张不要依赖特定的接口与数据来源,此时就涉及到对相关数据的模拟,比如 Http 和 JDBC 的返回结果等,可以使用虚拟对象即 Mock 对象进行模拟,使得单元测试不在耦合。
  Mock 过程的使用前提:
  (1) 实际对象时很难被构造出来的
  (2) 实际对象的特定行为很难被触发
  (3) 实际对象可能当前还不存在,比如依赖的接口还没有开发完成等等。
  Mockito 官网:https://site.mockito.org 。Mockito 和 JUnit 一样是专门针对 Java 语言的 mock 数据框架,它与同类的 EasyMock 和 jMock 功能非常相似,但是该工具更加简单易用。
  Mockito 的特点:
  (1) 可以模拟类不仅仅是接口
  (2) 通过注解方式简单易懂
  (3) 支持顺序验证
  (4) 具备参数匹配器
  4.2 Mockito 使用
  maven 引入 spring-boot-starter-test 会自动将 mockito 引入到工程中。
  4.2.1 使用案例
  (1) 在之前的代码中在定义一个 BookService 接口,含义是借书接口,暂且不做实现
  public interface BookService {
      Book orderBook(String name);
  }
  (2) 在之前的 StudentService 类中新增一个 orderBook 方法,含义是学生预定书籍方法,其中实现内容调用上述的 BookService 的 orderBook 方法。
  public Book orderBook(String name) {
     return bookService.orderBook(name);
  }
  (3) 编写单元测试方法,测试 StudentService 的 orderBook 方法
  @Test
  public void orderBookTest() {
      Book expectBook = new Book(1L, "钢铁是怎样炼成的", "书架A01");
      Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectBook);
      Book book = studentService.orderBook("");
      System.out.println(book);
      Assert.assertTrue("预定书籍不符", expectBook.equals(book));
  }
  (4) 执行结果:
  (5) 结果解析
  上述内容并没有实现 BookService 接口的 orderBook (String name) 方法。但是使用 mockito 进行模拟数据之后,却通过了单元测试,原因就在于 Mockito 替换了本来要在 StudentService 的 orderBook 方法中获取的对象,此处就模拟了该对象很难获取或当前无法获取到,用模拟数据进行替代。
  4.2.2 相关语法
  常用 API:
  上述案例中用到了 mockito 的 when、any、theWhen 等语法。接下来介绍下都有哪些常用的 API:
  1) mock:模拟一个需要的对象
  2) when:一般配合 thenXXX 一起使用,表示当执行什么操作之后怎样。
  3) any: 返回一个特定对象的缺省值,上例中标识可以填写任何 String 类型的数据。
  4) theReturn: 在执行特定操作后返回指定结果。
  5) spy:创造一个监控对象。
  6) verify:验证特定的行为。
  7) doReturn:返回结果。
  8) doThrow:抛出特定异常。
  9) doAnswer:做一个自定义响应。
  10) times:操作执行次数。
  11) atLeastOnce:操作至少要执行一次。
  12) atLeast:操作至少执行指定的次数。
  13) atMost:操作至多执行指定的次数。
  14) atMostOnce:操作至多执行一次。
  15) doNothing:不做任何的处理。
  16) doReturn:返回一个结果。
  17) doThrow:抛出一个指定异常。
  18) doAnswer:指定一个特定操作。
  19) doCallRealMethod:用于监控对象返回一个真实结果。
  4.2.3 使用要点
  (1) 打桩
  Mockito 中有 Stub,所谓存根或者叫打桩的概念,上面案例中的 Mockito.when (bookService.orderBook (any (String.class))).thenReturn (expectBook); 就是打桩的含义,先定义好如果按照既定的方式调用了什么,结果就输出什么。然后在使用 Book book = studentService.orderBook (""); 即按照指定存根输出指定结果。
  @Test
      public void verifyTest() {
          List mockedList = mock(List.class);
          mockedList.add("one");
          verify(mockedList).add("one");// 验证通过,因为前面定义了这个桩
          verify(mockedList).add("two");// 验证失败,因为前面没有定义了这个桩
      }
  (2) 参数匹配
  上例 StudentService 的 orderBook 方法中的 any (String.class) 即为参数匹配器,可以匹配任何此处定义的 String 类型的数据。
  (3) 次数验证
  @Test
      public void timesTest() {
          List mockedList = mock(List.class);
          when(mockedList.get(anyInt())).thenReturn(1000);
          System.out.println(mockedList.get(1));
          System.out.println(mockedList.get(1));
          System.out.println(mockedList.get(1));
          System.out.println(mockedList.get(2));
          // 验证通过:get(1)被调用3次
          verify(mockedList, times(3)).get(1);
          // 验证通过:get(1)至少被调用1次
          verify(mockedList, atLeastOnce()).get(1);
          // 验证通过:get(1)至少被调用3次
          verify(mockedList, atLeast(3)).get(1);
      }
  (4) 顺序验证
  @Test
      public void orderBookTest1() {
      String json = "{"id":12,"location":"书架A12","name":"三国演义"}";
      String json1 = "{"id":21,"location":"书架A21","name":"水浒传"}";
          String json2 = "{"id":22,"location":"书架A22","name":"红楼梦"}";
          String json3 = "{"id":23,"location":"书架A23","name":"西游记"}";
          when(bookService.orderBook("")).thenReturn(JSON.parseObject(json, Book.class));
          Book book = bookService.orderBook("");
          Assert.assertTrue("预定书籍有误", "三国演义".equals(book.getName()));
          when(bookService.orderBook("")).thenReturn(JSON.parseObject(json1, Book.class)).
                  thenReturn(JSON.parseObject(json2, Book.class)).
                  thenReturn(JSON.parseObject(json3, Book.class));
          Book book1 = bookService.orderBook("");
          Book book2 = bookService.orderBook("");
          Book book3 = bookService.orderBook("");
          Book book4 = bookService.orderBook("");
          Book book5 = bookService.orderBook("");
          // 全部验证通过,按顺序最后打桩打了3次,大于3次按照最后对象输出
          Assert.assertTrue("预定书籍有误", "水浒传".equals(book1.getName()));
          Assert.assertTrue("预定书籍有误", "红楼梦".equals(book2.getName()));
          Assert.assertTrue("预定书籍有误", "西游记".equals(book3.getName()));
          Assert.assertTrue("预定书籍有误", "西游记".equals(book4.getName()));
          Assert.assertTrue("预定书籍有误", "西游记".equals(book5.getName()));
  }
  (5) 异常验证
  @Test(expected = RuntimeException.class)
      public void exceptionTest() {
          List mockedList = mock(List.class);
          doThrow(new RuntimeException()).when(mockedList).add(1);
          // 验证通过
          mockedList.add(1);
      }
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号