• 单元测试Junit5+Mockito3+Assertj


    单元测试介绍与实践

    为什么单元测试

    • 天然的方法说明文档
    • 代码质量的保证
    • 持续重构的定心丸

    什么是好的单元测试

    • 单元测试需要自动化执行(CI)
    • 单元测试需要快速执行
      • 避免改代1行代码,单测跑5分钟的情况,谁也不愿意等
    • 单元测试不应该依赖测试的执行顺序,UT相互之间是独立的
    • 单元测试不应该依赖数据库,文件IO或任何长耗时任务。相反,单元测试需要与外部依赖隔离。
    • 单元测试是持续稳定的,在任何时候,任何环境中都是可执行的
    • 单元测试要有意义
      • get,set没必要写ut,核心的业务逻辑要写
    • 单元测试要尽量简短,及时维护
      • 太长太复杂的代码谁也不愿意看,看看是不是代码结构有问题,拆分逻辑单一职责原则(Single Responsibility Principle,SRP)

    单元测试原则

    AIR原则:

    • Automatic自动化
      • UT应该全部自动执行
      • 不能是通过手动打log验证UT结果,应该使用断言来验证
    • Independent独立性
      • UT用例之间不匀速相互调用,也不允许有执行次序的依赖
    • Repeatable可重复性
      • 不受外界环境的影响,尤其是一些网络,服务,中间件等的依赖,要和环境解耦

    测试什么

    • 某个类或者函数,粒度要足够小
    • 常用的输入输出组合
      • 正确的输入,得到正确的输出
      • 错误的输入(如非法数据,异常流程,非业务允许输入等)得到预期的错误结果
    • 边界条件和异常
      • 空值,特殊取值,特殊时间点,数据顺序,循环边界等

    单测覆盖率

    • 行覆盖

      • 统计单测执行了多少行
    • 分支覆盖

      • 统计逻辑分支是否都覆盖
    • 条件判定覆盖

      • 所有的条件每种可能都执行一次,同时每种条件的结果也都至少执行一次

    好用的测试框架

    Junit5

    常用注解

    注解 说明
    @Test 表示方法是测试方法,JUnit5中去掉了该注解的timeout参数支持
    @BeforeEach 在每一个测试方法运行前,都运行一个指定的方法
    @AfterEach 表示在每个单元测试之后执行
    @BeforeAll 表示在所有单元测试之前执行
    @AfterAll 表示在所有单元测试之后执行
    @Tag 表示单元测试类别
    @Disabled 表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
    @Timeout 表示测试方法运行如果超过了指定时间将会返回错误
    @ExtendWith 为测试类或测试方法提供扩展类引用
    @DisplayName 为测试类或者测试方法设置展示名称
    @RepeatedTest 表示方法可重复执行
    @ParameterizedTest **:*表示方法是参数化测试

    详细注解:https://junit.org/junit5/docs/current/user-guide/

    示例

    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.*;
    
    import java.util.Arrays;
    import java.util.concurrent.TimeUnit;
    
    class SortTest {
    
        @BeforeEach
        void init() {
            System.out.println("create connections...");
        }
    
        @AfterEach
        void destroy() {
            System.out.println("close connections...");
        }
    
        @Test
        @Tag("fast")
        @DisplayName("测试插入排序")
        @RepeatedTest(5)
        void insertSort() {
            int[] arr = {3, 1, 2, 5, 4};
            int[] result = Sort.insertSort(arr);
            Arrays.sort(arr);
            Assertions.assertThat(result)
                    .isSorted()
                    .as("测试插入排序");
        }
    
        @Test
        @Tag("fast")
        @DisplayName("测试插入排序--输入为null")
        void insertSortNullArr() {
            int[] arr = null;
            int[] result = Sort.insertSort(arr);
    
            Assertions.assertThat(result)
                    .isEqualTo(null)
                    .as("测试插入排序,输入为null,输出为null");
        }
    
        @Test
        @DisplayName("过期的测试")
        @Disabled
        void insertSortDisabled() {
            //我过期了
        }
    
        @Test
        @DisplayName("超时测试")
        @Timeout(value = 1, unit = TimeUnit.SECONDS)
        public void timeoutTest() throws InterruptedException {
            TimeUnit.SECONDS.sleep((long) 0.9);
            //如果测试方法时间超过1s将会异常
    //        TimeUnit.SECONDS.sleep((long) 1.9);
        }
    }
    

    todo Junit5测试SpringBoot

    Mockito3

    示例

    • 一版流程:given-when-then
    @Test
    public void thenReturn() {    
       //mock一个List类    
        List<String> list = mock(List.class);    
        //预设当list调用get()时返回hello,world    
        Mockito.when(list.get(anyInt())).thenReturn("hello,world");    
        String result = list.get(0);    
        Assert.assertEquals("hello,world", result);
    }
    
    • 验证行为是否发生
    //模拟创建一个List对象
    List<Integer> mock =  Mockito.mock(List.class);
    //调用mock对象的方法
    mock.add(1);
    mock.clear();
    //验证方法是否执行
    Mockito.verify(mock).add(1);
    Mockito.verify(mock).clear();
    
    • 多次触发返回不同值
    //mock一个Iterator类
    List<String> list = mock(List.class);
    //预设当list调用get()时第一次返回hello,第n次都返回world
    Mockito.when(list.get(anyInt())).thenReturn("hello").thenReturn("world");
    // 下面这句和上面的语句等价
    Mockito.when(list.get(anyInt())).thenReturn("hello","world");
    
    
    //使用mock的对象
    String result = list.get(0) + " " + list.get(1) + " " + list.get(2);
    //验证结果
    Assert.assertEquals("hello world world",result);
    
    • 模拟抛出异常
    @Test(expected = IOException.class)//期望报IO异常
    public void when_thenThrow() throws IOException{    
        OutputStream mock = Mockito.mock(OutputStream.class);    
        //预设当流关闭时抛出异常    
        Mockito.doThrow(new IOException()).when(mock).close();    
        mock.close();
    }
    
    • 匹配任意参数

    Mockito.anyInt() 任何 int 值 ;
    Mockito.anyLong() 任何 long 值 ;
    Mockito.anyString() 任何 String 值 ;

    Mockito.eq(Object) 任何匹配值 ;

    Mockito.any(XXX.class) 任何 XXX 类型的值 ;

    Mockito.any() 任何对象 等

    List list = Mockito.mock(List.class);
    //匹配任意参数
    Mockito.when(list.get(Mockito.anyInt())).thenReturn(1);
    Assert.assertEquals(1,list.get(1));Assert.assertEquals(1,list.get(999));
    

    注意:使用了参数匹配,那么所有的参数都必须通过matchers来匹配

    @Test
    public void matchers() {    
        Comparator comparator = mock(Comparator.class);   
        comparator.compare("nihao", "hello");    
        //如果你使用了参数匹配,那么所有的参数都必须通过matchers来匹配    
        Mockito.verify(comparator).compare(anyString(), Mockito.eq("hello"));    
        //下面的为无效的参数匹配使用    
        // Mockito.verify(comparator).compare(anyString(),"hello");
    }
    
    • void方法
    @Test
    public void Test() {    
    	A a = Mockito.mock(A.class);    
        //void 方法才能调用doNothing()    
        Mockito.doNothing().when(a).setName("bb");   
        a.setName("bb");   
        // 断言失败    
        Assert.assertEquals("bb",a.getName());
    }
    class A {    
        private String name;    
        public void setName(String name){
        this.name = name;    
        }    
        public String getName(){        
            return name;    
        }
    }
    
    • 预期回调接口生成期望值
    @Test
    public void answerTest(){
          List mockList = Mockito.mock(List.class);
          //使用方法预期回调接口生成期望值(Answer结构)
          Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new CustomAnswer());
          Assert.assertEquals("hello world:0",mockList.get(0));
          Assert.assertEquals("hello world:999",mockList.get(999));
      }
      private class CustomAnswer implements Answer<String> {
          @Override
          public String answer(InvocationOnMock invocation) throws Throwable {
              Object[] args = invocation.getArguments();
              return "hello world:"+args[0];
          }
      }
    等价于:(也可使用匿名内部类实现)
    @Test
    public void answer_with_callback() {
        //使用Answer来生成我们我们期望的返回
        Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Object[] args = invocation.getArguments();
                return "hello world:"+args[0];
            }
        });
        Assert.assertEquals("hello world:0",mockList.get(0));
        Assert.assertEquals("hello world:999",mockList.get(999));
    }
    

    AssertJ

    示例

    其他

    • 注意bytebuddy的版本,mockito3.3.3 依赖 bytebuddy1.10.5
    <!-- 单元测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy</artifactId>
        <version>1.10.5</version>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.8.0</version>
        <scope>test</scope>
    </dependency>
    <!--   单测结束     -->
    
    • maven执行测试用例的插件,保证流水线执行测试:
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M3</version>
        <configuration>
            <excludes>
                <exclude>some test to exclude here</exclude>
            </excludes>
        </configuration>
    </plugin>
    
    • 集成JaCoCo代码测试覆盖率统计

    个人理解

    • UT代码要简单;
    • 善用Mock;
    • UT的命名和注释很重要;
    • UT并不能保证完全没bug;
    • 如果发现单元测试不好写,首先怀疑是代码有问题,优先选择重构代码,实在不行再考虑使用框架的高级特性(如PowerMock可以mock私有方法,static方法,new变量等,这些特性理论上增加了写垃圾代码的可能 );
    • UT是体力话,写UT时间和代码开发时间相当;
    • UT是一件磨刀不误砍柴工的事,长远收益。故障越早发现,修复成本越低;
    • UT会倒逼开发人员白盒地思考代码逻辑和结构,更好地对代码进行设计,我认为这也是UT最重要的意义
    • 重构的时候心里有点底(我认为代码应该随时重构,哪怕一个变量名)
    • 写单测是为了证明程序有错,而不是程序无错

    后续

    基于内存数据库的单元测试...

    参考

    https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html Mockito官方文档

    https://junit.org/junit5/docs/current/user-guide/ JUnit5 User Guide

    https://time.geekbang.org/column/article/185684 极客时间-对于单元测试和重构的理解

    https://www.liaoxuefeng.com/wiki/1252599548343744/1304065789132833

    https://www.jianshu.com/p/ecbd7b5a2021

  • 相关阅读:
    golang实现单链表
    koa中间执行机制
    vuex源码简析
    从浏览器渲染过程看重绘回流
    javascript的this
    js 设计模式:观察者和发布订阅模式
    H5 移动端 键盘遮挡焦点元素解决方案
    webpack4 css modules
    Daily,一个入门级的 React Native 应用
    javascript: 类型转换
  • 原文地址:https://www.cnblogs.com/SimonZ/p/15471904.html
Copyright © 2020-2023  润新知