开发必备之单元测试

祸乱生于疏忽 单元测试先于交付。穿越暂时黑暗的时光隧道,才能迎来系统的曙光。

单元测试的相关介绍

​ 计算机世界里的软件产品通常是由模块组合而成的 模块又可以分成诸多子模块。 比如淘宝系统由搜索模块、商品模块、交易模块等组成,而交易模块又分成下单模块、 支付模块、发货模块等子模块,如此细分下去,最终的子模块是由不可再分的程序单 元组成的。对这些程序单元的测试,即称为单元测试(Unit Testing ,简称单测)。单元的粒度要根据实际情况判定,可能是类、方法等,在面向对象编程中,通常认为最小单元就是方法。单元测试的目的是在集成测试和功能测试之前对软件中的可测试单 元进 逐一检查和验证。单元测试是程序功能的基本保障,是软件产品上线非常重要的环。

​ 虽然单元测试的概念众所周知,但是能够深入理解的人却屈指可数 道的工程师更是凤毛麟角。在很多人看来,单元测试是一件功不在当下的事情,快速完成业务功能开发才是王道,特别是在评估工作量的时候,如果开发工程师说需要额外时间来写单测,并因此延长项目工期,估计有些项目经理就接捺不住了。其实单元测试是一件有情怀、有技术素养、有长远收益的工作,它是保证软件质量和效率的重要手段之一。单元测试的好处包括但不限于以下几点:

​ 单元测试的好处不言而喻,同时我们也要摒 诸如单元 试是测试 员的工作 单元测试代码不需要维护等常见误解。对于开发工程师来说 编写并维护单元测试不 仅仅为了保证代码的正确性 更是一种基本素养的体现。

单元测试的基本原则

​ 宏观上,单元测试要符合 AIR 原则;微观上 单元测试的代码层面要符合 BCDE 原则。

​ AIR 即空气 单元测试亦是如此。当业务代码在线上运行时 可能感觉不到测试用例的存在和价值,但在代码质 的保障上,却是非常关键的。新增代码应该同步新增测试用例,修改代码逻辑时也应该同步测试用例成功执行。 AIR 具体包括 :

  • A : Automatic (自动化)
  • I : Independent (独立性)
  • R : Repeatable (可重复)

​ 单元测试应该是全自动执行的。测试用例通常会被频繁地触发执行 执行过程必须完全自动化才有意义 如果单元测试的输出结果需要人工介入检查,那么它一定是不合格的。单元测试中不允许使用 System.out 来进行人工验证,而必须使用断言来验证。

​ 为了保证单元测试稳定可靠且便于维护,需要保证其独立性。用例之间不允许互相调用,也不允许出现执行次序的先后依赖。如下警示代码所示,testMethod2 需要调用 testMethod1。在执行 testMethod2 时会重复执行验证testMethod1,导致运行效率降低。更严重的是,testMethod1的验证失败会影响 testMethod2 的执行。

@Test
public void testMethod1() {
    
}

@Test
public void testMethod2 () {
    testMethod1();
    
}

​ 在主流测试框架中, JUnit 的用例执行顺序是无序的,而 TestNG 支持测试用例的顺序执行(默认测试类内部各测试用例是按字典序升序执行的,也可以通过XML或注解 priority 的方式来配置执行顺序)。

​ 单元测试是可以重复执行的,不能受到外界环境的影响。比如,单元测试通常会被放到持续集成中,每次有代码提交时单元测试都会被触发执行。如果单测对外部环境(网络、服务、中间件等)有依赖 ,则容易导致持续集成机制的不可用。 编写单元测试时要保证测试粒度足够小,这样有助于精确定位问题,单元测试 用例默认是方法级别的。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试需要覆盖的范围。编写单元测试用例时,为了保证被测模块的交付质量,需要符合BCDE原则:

  • B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
  • C: Correct,正确的输入,并得到预期的结果。
  • D: Design,与设计文档相结合,来编写单元测试。
  • E : Error,单元测试的目标是证明程序有错,而不是程序无错。为了发现代代码中潜在的错误 我们需要在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果。 由于单元测试只是系统集成测试前的小模块测试,有些因素往往是不具备的,因 此需要进行Mock,例如:
  • 功能因素。 比如被测试方法内部调用的功能不可用。
  • 时间因素。 比如双十一还没有到来,与此时间相关的功能点。
  • 环境因素。 政策环境,如支付宝政策类新功能,多端环境 PC 、手机等。
  • 数据因素。 线下数据样本过小,难以覆盖各种线上真实场景。
  • 其他因素。 为了简化测试编写,开发者也可以将某些复杂的依赖采用 Mock 方式实现

​ 最简单的 Mock 方式是硬编码,更为优雅的方式是使用配置文件,最佳的方式是使用相应的 Mock 框架,例如 JMockit、EasyMock、JMock 等。 Mock 的本质是让我们写出更加稳定的单元测试 隔离上述因素对单元测试的影响 使结果变得可预测,做到真正的”单元”测试。

单元测试的编写

单元测试编写是开发工程师的日常工作之一,利用好各种测试框架并掌握好单元测试编写技巧,往往可以达到事半功倍的效果。本节主要介绍如何编写 JUnit 测试用例。 我们先简要了解一下 JUnit 单元测试框架。

Java 语言的单元测试框架相对统一,JUnit和TestNG 几乎始终处于市场前两位。 其中 JUnit 以较长的发展历史和源源不断的功能演进,得到了大多数用户的青睐,也是阿里内部目前使用最多的单元测试框架。 JUnit项目的起源可以追溯到 1997 年。两位参加”面向对象程序系统语言 和应用大会”( Conference for Object-Oriented Programming Systems, Languages & Applications )的极客开发者 Kent Beck和Erich Gamma 在从瑞士苏黎世飞往美国亚特兰大的飞机上,为了打发长途飞行的无聊时间,他们聊起了对当时 Java 测试过程中缺乏成熟工具的无奈,然后决定一起设计一款更好用的测试框架,于是采用结对编程的方式在飞机上完成了 JUnit 雏形,以及世界上第一个 JUnit单元测试用例。经过 20 余年的发展和几次重大版本的跃迁, JUnit 2017 月正式发布了 5.0 定版本。 JUnit5对JDK8 及以上版本有了更好的支持(如增加了对Lambda 表达式的支持), 并且加入了更多的测试形式,如重复测试、参数化测试等。因此本书的测试用例会使 JUnit5 采编写,部分写法如果在 JUnit4 中不兼容,则会提前说明。

JUnit5.x 由以下三个主要模块组成:

  • JUnit Platform: 用于在 JVM 上启动测试框架,统一命令行、 Gradle和Maven等方式执行测试的入口
  • JUnit Jupiter:包含 JUnit5.x 全新的编程模型和扩展机制。
  • JUnit Vintage:用于在新的框架中兼容运行 JUnit3.x和JUnit4.x的测试用例。 为了便于开发者将注意力放在测试编写上,即不必关心测试的执行流程和结果展示,JUnit 提供了一些辅助测试的注解,常用的测试注解说明如下表所示:

下面是个典型的 JUnit5 测试类结构:

// 定义一个测试类并指定用例在测试报告中展示名称
@DisplayName("售票器类型测试")
public class TicketSellerTest {
    // 定义一个待测类的实例
    private TicketSeller ticketSeller;

    /**
     * 定义在整个测试类开始前执行的操作
     * 通常包括全局和外部资源(包括测试桩)的创建和初始化
     */
    @BeforeAll
    public static void init() {
        // doSomeThing...

    }

    /**
     * 定义在整个测试类完成后执行的操作
     * 通常包括全局和外部资源的释放或销毁
     */
    @AfterAll
    public static void cleanup() {
        // doSomeThing...

    }

    /**
     * 定义在每个测试用例开始前执行的操作
     * 通常包括基础数据和运行环境的准备
     */
    @BeforeEach
    public void create() {
        this.ticketSeller = new TicketSeller();
        // doSomeThing...

    }

    /**
     * 定义在每个测试用例完成后执行的操作
     * 通常包括运行环境的清理
     */
    @AfterEach
    public void destroy() {
        // doSomeThing...

    }

    /**
     * 测试用例,当车票售出后余票应减少
     */
    @Test
    @DisplayName("售票后余票应减少")
    public void shouldReduceInventoryWhenTicketSoldOut() {
        ticketSeller.setInventory(10);
        ticketSeller.sell(1);
        assertThat(ticketSeller.getInventory()).isEqualTo(9);
    }

    /**
     * 测试用例,当余票不足时应该报错
     */
    @Test
    @DisplayName("余票不足应报错")
    public void shouldThrowExceptionWhenNoEnoughInventory() {
        ticketSeller.setInventory(0);
        assertThatExceptionOfType(TicketException.class)
                .isThrownBy(() -> ticketSeller.sell(1))
                .withMessageContaining("all ticket sold out")
                .withNoCause();
    }

    /**
     * Disabled注解将禁用测试用例
     * 该测试用例会出现在最终的报告中,但不会被执行
     */
    @Disabled
    @Test
    @DisplayName("有退票时余票应增加")
    public void shouldIncreaseInventoryWhenTicketRefund() {
        ticketSeller.setInventory(10);
        ticketSeller.refund(1);
        assertThat(ticketSeller.getInventory()).isEqualTo(11);
    }

}

若是使用SpringBoot基于JUnit进行单元测试时,需要注意JUnit4和JUnit5的差异,如下:

在JUnit4中:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
    @Test
    public void contextLoads() {
    }
}

在JUnit5中:

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class ApplicationTests {
    @Test
    public void contextLoads() {
    }
}

​ 当定义好了需要运行的测试方法后,下一步则是关注测试方法的细节处理, 这就离不开断言(assert )和假设( assume):断言封装好了常用的判断逻辑 ,当不满足条件时,该测试用例会被认定为测试失败,假设与断言类似,只不过当条件不满足时,测试会直接退出而不是认定为测试失败,最终记录的状态是跳过。断言和假设是单元测试中最重要的部分,各种单元测试框架均提供了丰富的方法。以 JUnit 为例,它提供了一系列经典的断言和假设方法。

方法 释义 fail 断言测试失败 assertTrue/assertFalse 断言条件为真或为假 assertNull/assertNotNull 断言指定值为NULL或非NULL assertEquals/assertNotEquals 断言指定两个值相等或者不相等,对于基本数据类型,使用值比较;对于对象,使用equals方法对比 assertArrayEquals 断言数组元素全部相等 assertSame/assertNotSame 断言指定两个对象是否为同一个对象 assertThrows/assertDoesNotThrows 断言是否抛出了一个特定类型的异常 assertTimeout/assertTimeoutPreemptively 断言是否执行超时,区别在于测试程序是否在同一个线程内执行 assertIterableEquals 断言迭代器中的元素全部相等 assertLinesMatch 断言字符串列表元素是否全部正则匹配 assertAll 断言多个条件同时满足

相较于断言,假设提供的静态方法更加简单,被封装在 org.junit.jupiter.api. Assumptions 类, 同样为静态方法,如下表所示:

方法 释义 assumeTrue

assumeFalse 先判断给定的条件为真或假,再决定是否继续接下来的测试

​ 相对于假设,断言更为重要。这些断言方法中的大多数从 JUnit 的早期版本就已经存在,并且在最新的 JUnit5 版本中依然保持着很好的兼容性。当断言中指定的条件不满足时,测试用例就会被标记为失败。

​ 对于断言的选择,优先采用更精确的断言,因为它们通常提供了更友好的结果输出格式(包括预期值和实际值),例如 assetEquas(100, result) 语句优于 assertTrue(100 == result)语旬。对于非相等情况的判定,比如大于、小于或者更复杂的情况 可以使用 assertTrue、assertFalse 表达,例如 ssertTrue (result > 0)。对于特别复杂的条件判定,直接使用任何一种断言方法都不容易表达时,则可以使用 Java 语句自行构造条件,然后在不符合预期的情况下直接使用 fail 断言方法将测试标记为失败。另外值得强调的是,对于所有两参数的断言方法,例如 assertEquals、assertSame 第一个参数是预期的结果值,第二个参数才是实际的结果值。例如:

assertEquals(0, transactioMaker.increase(10).reduce(10)),假如测试结果错误,将会在测试报告中产生如下内容:

org.opentest4j.AssertionFailedError:
Expected : 0
Actual : 20

倘若将参数的位置写反,则生成报告的预期值与实际值位置也会颠倒,从而给阅读者带来困扰。

assertTimeout和assertTimoutPreemptively 断言的差异在于,前者会在操作超时后继续执行,并在最终的测试报告中记录操作的实际执行时间;后者在到达指定时间后立即结束,在最终的报告中只体现出操作超时,但不包含实际执行的耗时。

例如 使用 assertTimeout 断言的错误报告:

org.opentest4j.AssertionFailedError: execution exceeded timeout of 1000 ms by 5003 ms

使用 assertTime utPre mp ivel 断言的错误报告:

org.opentest4j.AssertionFailedError: execution timed out after 1000 ms

​ 断言负责验证逻辑以及数据的合法性和完整性,所以有一种说法,在单元测试方法中没有断言就不是完整的测试 !而在实际开发过程中,仅使用 JUnit 的断言 往往不能满足需求,要么是被局限在 JUnit 仅有的几种断言中,对于不支持的断言就不再写额外的判断逻辑,要么花费很大的精力,对要判断的条件经过一系列改造后,再使用 JUnit 现有的断言。有没有第三种选择?答案是:有的

AssertJ 的最大特点是流式断言(Fluent Assertions),与 Build Chain 模式或 Java8 的stream&filter 写法类似。它允许一个目标对象通过各种 Fluent Assertions API的连接判断,进行多次断言,并且对 IDE 更友好。但是 AssertJ的assertThat 的处理方法和之前有些不同,它利用 Java 的泛型,同时增加了目标类型对应的 XxxxAssert类,签名为 public static AbstractCharSequenceAssert<?,String> assertThat(String acutal),而 JUnit 中的 public static void assertThat&#xFF08;&#xFF09; 是void 返回,其中, AbstractCharSequenceAssert 是针对 String 对象的,这样不同的类型有不同断言方法,如String和Date 就有不一样的断言方法。

下面通过一个例子,来一起认识一下强大的 AssertJ。首先使用 JUnit 的经典断言实现一段测试:

/**
 * 使用Junit的断言
 */
public class JUnitSampleTest {

    @Test
    public void testUsingJUnitAssertThat() {
        // 字符串判断
        String s = "abcde";
        Assertions.assertTrue(s.startsWith("ab"));
        Assertions.assertTrue(s.endsWith("de"));
        Assertions.assertEquals(5,s.length());

        // 数字判断
        Integer i = 50;
        Assertions.assertTrue(i > 0);
        Assertions.assertTrue(i < 100);

        // 日期判断
        Date date1 = new Date();
        Date date2 = new Date(date1.getTime() + 100);
        Date date3 = new Date(date1.getTime() - 100);
        Assertions.assertTrue(date1.before(date2));
        Assertions.assertTrue(date1.after(date3));

        // List判断
        List list = Arrays.asList("a", "b", "c", "d");
        Assertions.assertEquals("a",list.get(0));
        Assertions.assertEquals(4,list.size());
        Assertions.assertEquals("d",list.get(list.size() - 1));

        //Map判断
        Map map = new HashMap<>();
        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);
        Set set = map.keySet();
        Assertions.assertEquals(3, set.size());
        Assertions.assertTrue(set.containsAll(Arrays.asList("A","B","C")));
    }

}

下面,我们使用 AssertJ来完成同样的断言:

/**
 * 使用AssertJ断言
 */
public class AssertJSampleTest {

    @Test
    public void testUsingAssertJ() {
        // 字符串判断
        String s = "abcde";
        Assertions.assertThat(s).as("字段串判断:判断首位及长度")
                .startsWith("ab").endsWith("de").hasSize(5);

        // 数字判断
        Integer i = 50;
        Assertions.assertThat(i).as("数字判断:数字大小比较")
                .isGreaterThan(0).isLessThan(100);

        // 日期判断
        Date date1 = new Date();
        Date date2 = new Date(date1.getTime() + 100);
        Date date3 = new Date(date1.getTime() - 100);
        Assertions.assertThat(date1).as("日期判断:日期大小比较")
                .isBefore(date2).isAfter(date3);

        // List判断
        List list = Arrays.asList("a", "b", "c", "d");
        Assertions.assertThat(list).as("List的判断:首尾元素及长度")
                .startsWith("a").endsWith("d").hasSize(4);

        //Map判断
        Map map = new HashMap<>();
        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);
        Assertions.assertThat(map).as("Map的判断:长度及key值")
                .hasSize(3).containsKeys("A", "B", "C");
    }

}

Original: https://www.cnblogs.com/reminis/p/15256389.html
Author: 小懒编程日记
Title: 开发必备之单元测试

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/571486/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 删除重复值的结点

    删除重复值的结点 问题重述: 给定一个无序单链表的头节点head,删除其中值重复出现的结点 问题分析: 这道题要删除重复值的结点,我们可以想到哈希表,因为哈希表是无序不重复的,我们…

    Java 2023年6月7日
    066
  • java 初始化

    这里的主要内容是 &#x521D;&#x59CB;&#x5316;相关的内容,其中还会穿插其他的内容 构造器初始化 静态数据初始化 显示的静态初始化 非静态…

    Java 2023年6月5日
    0101
  • java集合框架学习笔记

    思维导图 ; 一、什么是集合 存放在java.util.*。是一个存放对象的容器。 存放的是对象的引用,不是对象本身 长度不固定 只能存放对象 二、collection接口 col…

    Java 2023年6月13日
    067
  • 大厂钟爱的全链路压测有什么意义?四种压测方案详细对比分析

    全链路压测? 基于实际的生产业务场景和系统环境,模拟海量的用户请求和数据,对整个业务链路进行各种场景的测试验证,持续发现并进行瓶颈调优,保障系统稳定性的一个技术工程。 针对业务场景…

    Java 2023年6月15日
    0101
  • Java中使用FTPClient上传下载

    在JAVA程序中,经常需要和FTP打交道,比如向FTP服务器上传文件、下载文件,本文简单介绍如何利用jakarta commons中的FTPClient(在commons-net包…

    Java 2023年5月29日
    086
  • Netty源码研究笔记(1)——开篇

    1.1. Netty介绍 Netty是一个老牌的高性能网络框架。在众多开源框架中都有它的身影,比如:grpc、dubbo、seata等。 里面有着非常多值得学的东西: I/O模型 …

    Java 2023年6月10日
    074
  • Spring StateMachine状态机

    一、状态机 有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。在电商场景(订单、物流、售后)、社交(…

    Java 2023年5月30日
    092
  • 进程通讯 & Binder机制 & Service 笔记

    每个 app 都处于不同进程,每启动一个 APP,默认会启动一个虚拟机上,一个虚拟机就是一个进程。 分享通过 intent 传递数据,成功后回到 app;当你需要把本地数据库对外提…

    Java 2023年6月7日
    0126
  • Jedis操作set&Sortedset和Jedis连接池

    集合类型 set:不允许重复元素 sadd smembers:获取所有元素 @Test public void MyTest04() { Jedis jedis = new Jed…

    Java 2023年6月6日
    068
  • mock测试出现Circular view path [trade_records]: would dispatch back to the current handler URL

    这是因为你的Controller中返回的视图名称与你当前的requestMapping名称一样,这并没有很好的解决方案,除非你改掉其中一个名字。 因为springframework…

    Java 2023年6月7日
    0137
  • dubbo源码分析4(spring配置文件解析机制)

    我们知道dubbo一般也不会单独使用的吧,都会和spring一起使用,知道为什么吗? 因为dubbo是基于spring的扩展机制进行扩展的,所以首先我们要知道spring提供了一种…

    Java 2023年6月6日
    088
  • 阿里巴巴编码规范-考试认证

    阿里巴巴编码规范-考试认证 雨打梨花深闭门,忘了青春,误了青春。 1、注册阿里云账号 2、购买认证 需要怒支付一顿早餐Q,可以用支付宝支付,选择支付宝支付然后直接输入支付密码就OK…

    Java 2023年6月5日
    0106
  • 从Spring中学到的【1】–读懂继承链

    最近看了一些 Spring 源码,发现源码分析的文章很多,而底层思想分析的文章比较少,这个系列文章准备总结一下Spring中给我的启示,包括设计模式思想、SOLID设计原则等,涉及…

    Java 2023年6月16日
    099
  • 3. SpringBoot整合Redis

    1.下载地址以及安装 https://github.com/MicrosoftArchive/redis/releases 我是安装的windows版本所以选择这个: 解压之后运行…

    Java 2023年6月9日
    0105
  • 【Java分享客栈】一文搞定京东零售开源的AsyncTool,彻底解决异步编排问题。

    一、前言 本章主要是承接上一篇讲CompletableFuture的文章,想了解的可以先去看看案例: https://juejin.cn/post/7091132240574283…

    Java 2023年6月9日
    0113
  • mysql查出指定时间段中的每天的日期

    背景:数据库中某个业务表产生的数据日期不连续的,比如出库表,本月5号和27号可能都没有出库记录。前端报表组件要求传入连续的日期以便渲染数据 需求:返回指定日期时间段内的每天的出库量…

    Java 2023年6月8日
    071
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球