Spring05 - 单元测试

单元测试

1:单元测试概述

1:什么是单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。

对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,总的来说,单元就是人为规定的最小的被测功能模块。

单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试

2:为什么要写单元测试

使用单元测试可以有效地降低程序出错的机率,提供准确的文档,并帮助我们改进设计方案等等。

  • 允许你对代码做出任何改变,因为你了解单元测试会在你的预期之中。
  • 单元测试可以有效地降低程序出现BUG的机率;
  • 帮助你更深入地理解代码,因为在写单元测试的时候,你需要明确程序所有的执行流程及对应的执行结果等等;
  • 允许在任何时候代码重构,而不必担心破坏现有的代码。这使得我们编写程序更灵活;
  • 确保你的代码的健壮性,因为所有的测试都是通过了的。
  • 文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
  • 具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。

3:什么时候写单元测试

  • 一是在具体实现代码之前,这是测试驱动开发(TDD)所提倡的
  • 二是与具体实现代码同步进行。先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。其实这种方案跟第一种已经很接近,基本上功能代码开发完,单元测试也差不多完成了。
  • 三是编写完功能代码再写单元测试。【实践经验证明,事后编写的单元测试“粒度”都比较粗。】

对同样的功能代码,采取前两种方案的结果可能是用10个“小”的单测来覆盖,每个单测比较简单易懂,可读性可维护性都比较好(重构时单测的改动不大);

而第三种方案写的单测,往往是用1个“大”的单测来覆盖,这个单测逻辑就比较复杂,因为它要测的东西很多,可读性可维护性就比较差。

所以比较推荐第二种

4:单元测试测试要写多细

单元测试不是越多越好,而是越有效越好!进一步解读就是哪些代码需要有单元测试覆盖:

  • 逻辑复杂的
  • 容易出错的
  • 不易理解的,即使是自己过段时间也会遗忘的,看不懂自己的代码,单元测试代码有助于理解代码的功能和需求
  • 公共代码。比如自定义的所有http请求都会经过的拦截器;工具类等。
  • 核心业务代码。一个产品里最核心最有业务价值的代码应该要有较高的单元测试覆盖率

5:单元测试黑话

5.1:被测系统(SUT)

被测系统(System under test, SUT)表示正在被测试的系统, 目的是测试系统能否正确操作。

根据测试类型的不同, SUT 指代的内容也不同, 例如 SUT 可以是一个类甚至是一整个系统。

5.2:测试依赖组件(DOC)

被测系统所依赖的组件, 例如进程 UserService 的单元测试时, UserService 会依赖 UserDao, 因此 UserDao 就是 DOC

5.3:测试替身(Test Double)

一个实际的系统会依赖多个外部对象, 但是在进行单元测试时, 我们会用一些功能较为简单的并且其行为和实际对象类似的假对象来作为 SUT 的依赖对象, 以此来降低单元测试的复杂性和可实现性。

在这里, 这些假对象就被称为 测试替身(Test Double). 测试替身有如下 5 种类型:

Test stub

假设我们的一个模块需要从 HTTP 接口中获取商品价格数据, 这个获取数据的接口被封装为 getPrice 方法。

在对这个模块进行测试时, 我们显然不太可能专门开一个 HTTP 服务器来提供此接口, 而是提供一个带有 getPrice 方法的假对象, 从这个假对象中获取数据。

在这个例子中, 提供数据的假对象就叫做 Test stub.

Fake object

实现了简单功能的一个假对象. Fake object 和 Test stub 的主要区别就是 Test stub 侧重于用于提供数据的假对象, 而 Fake object 没有这层含义.

使用 Fake object 的最主要的原因就是在测试时某些组件不可用或运行速度太慢, 因而使用 Fake object 来代替它们.

Mock object

用于模拟实际的对象, 并且能够校验对这个 Mock object 的方法调用是否符合预期.

实际上, Mock object 是 Test stub 或 Fake object 一种, 但是 Mock object 有 Test stub/Fake object 没有的特性

Mock object 可以很灵活地配置所调用的方法所产生的行为, 并且它可以追踪方法调用

例如一个 Mock Object 方法调用时传递了哪些参数, 方法调用了几次等

Dummy object

在测试中并不使用的, 但是为了测试代码能够正常编译/运行而添加的对象。

例如我们调用一个 Test Double 对象的一个方法, 这个方法需要传递几个参数, 但是其中某个参数无论是什么值都不会影响测试的结果, 那么这个参数就是一个 Dummy object。Dummy object 可以是一个空引用, 一个空对象或者是一个常量等。

简单的说, Dummy object 就是那些没有使用到的, 仅仅是为了填充参数列表的对象。

test Spy

可以包装一个真实的 Java 对象, 并返回一个包装后的新对象。

若没有特别配置的话, 对这个新对象的所有方法调用, 都会委派给实际的 Java 对象。

mock 和 spy 的区别是: mock 是无中生有地生出一个完全虚拟的对象, 它的所有方法都是虚拟的;

而 spy 是在现有类的基础上包装了一个对象, 即如果我们没有重写 spy 的方法, 那么这些方法的实现其实都是调用的被包装的对象的方法

5.4:Test fixture

所谓 test fixture, 就是运行测试程序所需要的先决条件(precondition)。

即对被测对象进行测试时所需要的一切东西,这个东西不单单指的是数据, 同时包括对被测对象的配置, 被测对象所需要的依赖对象等。

JUnit4 之前是通过 setUp, TearDown 方法完成, 在 JUnit4这, 我们可以使用 @Before 代替 setUp 方法, @After 代替 tearDown 方法.

注意, @Before 在每个测试方法运行前都会被调用, @After 在每个测试方法运行后都会被调用。

因为 @Before 和 @After 会在每个测试方法前后都会被调用, 而有时我们仅仅需要在测试前进行一次初始化,

这样的情况下, 可以使用@BeforeClass 和@AfterClass 注解

5.5:测试用例(Test case)

在 JUnit 3中, 测试方法都必须以 test 为前缀, 且必须是 public void 的

JUnit 4之后, 就没有这个限制了, 只要在每个测试方法标注 @Test 注解, 方法签名可以是任意的

5.6:测试套件

通过 TestSuit 对象将多个测试用例组装成一个测试套件, 测试套件批量运行。

通过@RunWith 和@SuteClass 两个注解, 我们可以创建一个测试套件。

通过@RunWith 指定一个特殊的运行器及 Suite.class 套件运行器, 并通过@SuiteClasses 注解, 将需要进行测试的类列表作作为参数传入

二:Junit4

官方网址:https://junit.org/junit4/

官方入门文档:https://github.com/junit-team/junit4/wiki/Assertions

官方Github:https://github.com/junit-team

1:常用注解

@Test

在junit3中,是通过对测试类和测试方法的命名来确定是否是测试,且所有的测试类必须继承junit的测试基类。

在junit4中,定义一个测试方法变得简单很多,只需要在方法前加上@Test就行了

⚠️ 测试方法必须是public void,即公共、无返回数据。可以抛出异常。

@Ignore

有时候我们想暂时不运行某些测试方法\测试类,可以在方法前加上这个注解。

在运行结果中,junit会统计忽略的用例数,来提醒你。

但是不建议经常这么做,因为这样的坏处时,容易忘记去更新这些测试方法,导致代码不够干净,用例遗漏。

使用此标注的时候不能与其它标注一起使用,如:和@Test 标注一起使用,那就没用了

@BeforeClasses

当我们运行几个有关联的用例时,可能会在数据准备或其它前期准备中执行一些相同的命令,这个时候为了让代码更清晰,更少冗余

可以将公用的部分提取出来,放在一个方法里,并为这个方法注解@BeforeClass。

意思是在测试类里所有用例运行之前,运行一次这个方法。例如创建数据库连接、读取文件等。

注意:方法名可以任意,但必须是public static void,即公开、静态、无返回。这个方法只会运行一次。

@afterClasses

跟@BeforeClass对应,在测试类里所有用例运行之后,运行一次。用于处理一些测试后续工作,例如清理数据,恢复现场。

注意:同样必须是public static void,即公开、静态、无返回。这个方法只会运行一次。

@before

与@BeforeClass的区别在于,@Before不止运行一次,它会在每个用例运行之前都运行一次。

主要用于一些独立于用例之间的准备工作。

比如两个用例都需要读取数据库里的用户A信息,但第一个用例会删除这个用户A,而第二个用例需要修改用户A。

那么可以用@BeforeClass创建数据库连接。用@Before来插入一条用户A信息。

⚠️ 必须是public void,不能为static。不止运行一次,根据用例数而定。

@after

与@Before对应

@Runwith

首先要分清几个概念:测试方法、测试类、测试集、测试运行器。

其中测试方法就是用@Test注解的一些函数。

测试类是包含一个或多个测试方法的一个Test.java文件。

测试集是一个suite,可能包含多个测试类。

测试运行器则决定了用什么方式偏好去运行这些测试集/类/方法。

而@Runwith就是放在测试类名之前,用来确定这个类怎么运行的。也可以不标注,会使用默认运行器。常见的运行器有:

  • @RunWith(Parameterized.class) 参数化运行器,配合@Parameters使用junit的参数化功能
  • @RunWith(Suite.class) @SuiteClasses({ATest.class,BTest.class,CTest.class})测试集运行器配合使用测试集功能
  • @RunWith(JUnit4.class) junit4的默认运行器
  • @RunWith(JUnit38ClassRunner.class) 用于兼容junit3.8的运行器
  • 一些其它运行器具备更多功能。例如@RunWith(SpringJUnit4ClassRunner.class)集成了spring的一些功能

2:编写单元测试

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
2.1:生命周期测试
public class StandardTest {
    @BeforeClass
    public static void beforeClass() {
        System.out.println("in before class");
    }
    @AfterClass
    public static void afterClass() {
        System.out.println("in after class");
    }
    @Before
    public void before() {
        System.out.println("in before");
    }
    @After
    public void after() {
        System.out.println("in after");
    }
    @Test
    public void testCase1() {
        System.out.println("in test case 1");
    }
    @Test
    public void testCase2() {
        System.out.println("in test case 2");
    }
}
2.2:禁用注解测试
/**
 * 暂不执行该方法
 */
@Ignore
@Test
public void ignoreTest(){
    System.out.println("ignore test");
}
2.3:断言测试
  • assertEquals() 如果比较的两个对象是相等的,此方法将正常返回;否则失败显示在JUnit的窗口测试将中止。
  • assertSame() 和 assertNotSame() 方法测试两个对象引用指向完全相同的对象。
  • assertNull() 和 assertNotNull() 方法测试一个变量是否为空或不为空(null)。
  • assertTrue() 和 assertFalse() 方法测试if条件或变量是true还是false。
  • assertArrayEquals() 将比较两个数组,如果它们相等,则该方法将继续进行不会发出错误。否则失败将显示在JUnit窗口和中止测试。
2.4:异常测试

Junit 用代码处理提供了一个追踪异常的选项。你可以测试代码是否它抛出了想要得到的异常。expected 参数和 @Test 注释一起使用

@Test(expected = ArithmeticException.class)
    public void exceptionTest() {
        System.out.println("in exception success test");
        int a = 0;
        int b = 1 / a;
    }

    @Test(expected = NullPointerException.class) // 是否抛出了空指针异常
    public void exceptionFailTest() {
        System.out.println("in exception fail test");
        int a = 0;
        int b = 1 / a;
    }
in exception success test
in exception fail test

java.lang.Exception: Unexpected exception, expected<java.lang.NullPointerException> but was<java.lang.ArithmeticException>

	at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:28)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
2.5:时间测试

JUnit提供了一个暂停的方便选项,如果一个测试用例比起指定的毫秒数花费了更多的时间,那么JUnit将自动将它标记为失败

timeout参数和@Test注解一起使用

@Test(timeout = 1000) // 如果下面的执行方法超过1s,直接失败
public void testCase1() throws InterruptedException {
    TimeUnit.SECONDS.sleep(5000);
    System.out.println("in timeout exception");
}

可以设置全局超时规则

@Rule
public Timeout globalTimeout = Timeout.seconds(5); // 每一个方法最多执行5秒
2.6:参数化测试

Junit 4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同 一个测试。你将遵循 5 个步骤来创建参数化测试:

  1. 为准备使用参数化测试的测试类指定特殊的运行器 org.junit.runners.Parameterized。
  2. 为测试类声明几个变量,分别用于存放期望值和测试所用数据。
  3. 为测试类声明一个带有参数的公共构造函数,并在其中为第二个环节中声明的几个变量赋值。
  4. 为测试类声明一个使用注解 org.junit.runners.Parameterized.Parameters 修饰的,返回值为 java.util.Collection 的公共静态方法,并在此方法中初始化所有需要测试的参数对。
  5. 编写测试方法,使用定义的变量作为参数进行测试。

什么是@RunWith?

首先要分清几个概念:测试方法、测试类、测试集、测试运行器。

  • 其中测试方法就是用@Test注解的一些函数。
  • 测试类是包含一个或多个测试方法的一个Test.java文件,
  • 测试集是一个suite,可能包含多个测试类。
  • 测试运行器则决定了用什么方式偏好去运行这些测试集/类/方法。

而@Runwith就是放在测试类名之前,用来确定这个类怎么运行的。也可以不标注,会使用默认运行器。常见的运行器有:

  • @RunWith(Parameterized.class) 参数化运行器,配合@Parameters使用JUnit的参数化功能
  • @RunWith(Suite.class) @SuiteClasses({ATest.class,BTest.class,CTest.class}) 测试集运行器配合使用测试集功能
  • @RunWith(JUnit4.class), junit4的默认运行器
  • @RunWith(JUnit38ClassRunner.class),用于兼容junit3.8的运行器 一些其它运行器具备更多功能。例如
  • @RunWith(SpringJUnit4ClassRunner.class)集成了spring的一些功能
// 有如下待测试方法
public class PrimeNumberChecker {
    // 判断一个数是不是素数
    public Boolean validate(final Integer parimeNumber) {
        for (int i = 2; i < (parimeNumber / 2); i++) {
            if (parimeNumber % i == 0) {
                return false;
            }
        }
        return true;
    }
}
package org.example;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

/**
 * <p>
 * 功能描述:参数运算器演示
 * </p>
 *
 * @author cui haida
 * @date 2024/04/18/7:25
 */
@RunWith(Parameterized.class) // 1:指定定参数运算器
public class PrimeNumberCheckTest {

    /**
     * 步骤二声明变量
     */
    private Integer inputNumber;
    private Boolean expectedResult;
    private PrimeNumberChecker primeNumberChecker;

    /**
     * 步骤三:为测试类声明一个带有参数的公共构造函数,为变量赋值
     */
    public PrimeNumberCheckTest(Integer inputNumber, Boolean expectedResult) {
        this.inputNumber = inputNumber;
        this.expectedResult = expectedResult;
    }

    /**
     * 步骤四:为测试类声明一个使用注解 org.junit.runners.Parameterized.Parameters 修饰的,返回值为
     * java.util.Collection 的公共静态方法,并在此方法中初始化所有需要测试的参数对
     * 1)该方法必须由Parameters注解修饰
     * 2)该方法必须为public static的
     * 3)该方法必须返回Collection类型
     * 4)该方法的名字不做要求
     * 5)该方法没有参数
     */
    @Parameterized.Parameters
    public static java.util.Collection<Object[]> data() {
        return java.util.Arrays.asList(new Object[][]{
                {2, true}, // if input is 2, expect res is true
                {6, false},
                {19, true},
                {22, false},
                {23, true}
        });
    }

    @Before
    public void initialize() {
        // 前置操作,创建构造方法
        primeNumberChecker = new PrimeNumberChecker();
    }

    /**
     * 步骤五:编写测试方法,使用自定义变量进行测试
     */
    @Test
    public void testPrimeNumberChecker() {
        System.out.println("Parameterized Number is : " + inputNumber);
        Assert.assertEquals(expectedResult,
                primeNumberChecker.validate(inputNumber));
    }
}

// 待测试方法
class PrimeNumberChecker {

    public Boolean validate(final Integer parimeNumber) {
        for (int i = 2; i < (parimeNumber / 2); i++) {
            if (parimeNumber % i == 0) {
                return false;
            }
        }
        return true;
    }
}

在这里插入图片描述

2.7:套件测试

“套件测试”是指捆绑了几个单元测试用例并运行起来。在JUnit中,@RunWith 和 @Suite 这两个注解是用来运行套件测试。先来创建几个测试类

public class JunitTest1 {
    @Test
    public void printMessage(){
        System.out.println("in JunitTest1");
    }
}
public class JunitTest2 {

    @Test
    public void printMessage(){
        System.out.println("in JunitTest2");
    }
}
/**
 * Test suite.
 */
@RunWith(Suite.class)
@Suite.SuiteClasses({JunitTest1.class,JunitTest2.class})
public class JunitSuiteTest {
}
2.8:测试顺序

自定义测试方法的顺序,比如按照方法的名字顺序:@FixMethodOrder(MethodSorters.NAME_ASCENDING)

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestMethodOrder {

    @Test
    public void testA() {
        System.out.println("first");
    }

    @Test
    public void testC() {
        System.out.println("third");
    }

    @Test
    public void testB() {
        System.out.println("second");
    }
}

三:Junit5

官网:https://junit.org/junit5/

官方入口文档:https://junit.org/junit5/docs/current/user-guide/#overview

官方例子:https://github.com/junit-team/junit5-samples

官方github:https://github.com/junit-team

与以前版本的JUnit不同,JUnit 5由三个不同子项目中的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform是基于JVM的运行测试的基础框架在,它定义了开发运行在这个测试框架上的TestEngine API。此外该平台提供了一个控制台启动器,可以从命令行启动平台,可以为Gradle和 Maven构建插件,同时提供基于JUnit 4的Runner。
  • JUnit Jupiter是在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合。提供了一个TestEngine在平台上运行基于Jupiter的测试。
  • JUnit Vintage提供了一个TestEngine在平台上运行基于JUnit 3和JUnit 4的测试。

1:常用注解

  • @Test 表示方法是一种测试方法。 与JUnit 4的@Test注解不同,此注释不会声明任何属性。

  • @ParameterizedTest 表示方法是参数化测试

  • @RepeatedTest 表示方法是重复测试模板

  • @TestFactory 表示方法是动态测试的测试工程
  • @DisplayName 为测试类或者测试方法自定义一个名称
  • @BeforeEach 表示方法在每个测试方法运行前都会运行 ,@AfterEach 表示方法在每个测试方法运行之后都会运行
  • @BeforeAll 表示方法在所有测试方法之前运行 ,@AfterAll 表示方法在所有测试方法之后运行
  • @Nested 表示带注解的类是嵌套的非静态测试类,@BeforeAll和 @AfterAll方法不能直接在@Nested测试类中使用,除非修改测试实例生命周期。
  • @Tag 用于在类或方法级别声明用于过滤测试的标记
  • @Disabled 用于禁用测试类或测试方法
  • @ExtendWith 用于注册自定义扩展,该注解可以继承
  • @FixMethodOrder(MethodSorters.NAME_ASCENDING),控制测试类中方法执行的顺序,这种测试方式将按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致;不过这种方式需要对测试方法有一定的命名规则,如测试方法均以testNNN开头(NNN表示测试方法序列号 001-999)

2:编写单元测试

<dependencies>
    <!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-launcher</artifactId>
        <version>1.7.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.7.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>5.7.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.7.0</version>
    </dependency>

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
    </dependency>
</dependencies>
@Test
void firstTest() {
    assertEquals(2, 1 + 1);
}

@Test注解在方法上标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。

JUnit 5不再需要手动将测试类与测试方法为public,包可见的访问级别就足够了(package can)。

2.1:测试生命周期

对比下Junit5和Junit4注解:

Junit4Junit5注释
@Test@Test表示该方法是一个测试方法
@BeforeClass@BeforeAll表示使用了该注解的方法应该在当前类中所有测试方法之前执行(只执行一次),并且它必须是 static方法(除非@TestInstance指定生命周期为Lifecycle.PER_CLASS)
@AfterClass@AfterAll表示使用了该注解的方法应该在当前类中所有测试方法之后执行(只执行一次),并且它必须是 static方法(除非@TestInstance指定生命周期为Lifecycle.PER_CLASS)
@Before@BeforeEach表示使用了该注解的方法应该在当前类中每一个测试方法之前执行
@After@AfterEach表示使用了该注解的方法应该在当前类中每一个测试方法之后执行
@Ignore@Disabled用于禁用(或者说忽略)一个测试类或测试方法
@Category@Tag用于声明过滤测试的tag标签,该注解可以用在方法或类上
public class StandardTest {
    @BeforeAll
    static void initAll() {
        System.out.println("BeforeAll");
    }
    @BeforeEach
    void init() {
        System.out.println("BeforeEach");
    }
    @Test
    void succeedingTest() {
        System.out.println("succeedingTest");
    }
    @Test
    void failingTest() {
        System.out.println("failingTest");
        fail("a failing test");
    }
    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
    }
    @Test
    void abortedTest() {
        System.out.println("abortedTest");
        assumeTrue("abc".contains("Z"));
        fail("test should have been aborted");
    }
    @AfterEach
    void tearDown() {
        System.out.println("AfterEach");
    }
    @AfterAll
    static void tearDownAll() {
        System.out.println("AfterEach");
    }
}
2.2:禁用测试
// ========= 直接禁用测试类 ========
@Disabled
class DisabledClassTest {
    @Test
    void testWillBeSkipped() {
    }
}

// 禁用测试某一个方法
lass DisabledTest {

    @Disabled
    @Test
    void testWillBeSkipped() {
    }
}
2.3:断言测试

准备好测试实例、执行了被测类的方法以后,断言能确保你得到了想要的结果

一般的断言,无非是检查一个实例的属性(比如,判空与判非空等),或者对两个实例进行比较(比如,检查两个实例对象是否相等)等。

无论哪种检查,断言方法都可以接受一个字符串作为最后一个可选参数,它会在断言失败时提供必要的描述信息。

如果提供出错信息的过程比较复杂,它也可以被包装在一个 lambda 表达式中,这样,只有到真正失败的时候,消息才会真正被构造出来

  • assertEquals 断言预期值和实际值相等
  • assertAll 分组断言,执行其中包含的所有断言
  • assertArrayEquals 断言预期数组和实际数组相等
  • assertFalse 断言条件为假
  • assertNotNull 断言不为空
  • assertSame 断言两个对象相等
  • assertTimeout 断言超时
  • fail 使单元测试失败
public class AssertionsTest {

    Person person = new Person("John", "Doe");

    @Test
    void standardAssertions() {
        assertEquals(2, 2);
        assertEquals(4, 4, "The optional assertion message is now the last parameter.");
        assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily.");
    }

    @Test
    void groupedAssertions() {
        // In a grouped assertion all assertions are executed, and any
        // failures will be reported together.
        assertAll("person",
                () -> assertEquals("John", person.getFirstName()),
                () -> assertEquals("Doe", person.getLastName())
        );
    }

    @Test
    void dependentAssertions() {
        assertAll("properties",
                () -> {
                    String firstName = person.getFirstName();
                    assertNotNull(firstName);

                    // Executed only if the previous assertion is valid.
                    assertAll("first name",
                            () -> assertTrue(firstName.startsWith("J")),
                            () -> assertTrue(firstName.endsWith("n"))
                    );
                },
                () -> {
                    String lastName = person.getLastName();
                    assertNotNull(lastName);

                    // Executed only if the previous assertion is valid.
                    assertAll("last name",
                            () -> assertTrue(lastName.startsWith("D")),
                            () -> assertTrue(lastName.endsWith("e"))
                    );
                }
        );
    }
}

assertTimeoutPreemptively()assertTimeout() 的区别为: 两者都是断言超时

  • 前者在指定时间没有完成任务就会立即返回断言失败
  • 后者会在任务执行完毕之后才返回。
2.4:异常测试

代码中对于带有异常的方法通常都是使用 try-catch 方式捕获处理,针对测试这样带有异常抛出的代码

而 JUnit 5 提供方法 Assertions#assertThrows(Class<T>, Executable) 来进行测试

第一个参数为异常类型,第二个为函数式接口参数,跟 Runnable 接口相似

不需要参数,也没有返回,并且支持 Lambda表达式方式使用

// 标准的测试例子
@Test
@DisplayName("Exception Test Demo")
void assertThrowsException() {
    String str = null;
    assertThrows(IllegalArgumentException.class, () -> {
        Integer.valueOf(str);
    });
}

// 注:异常失败例子,当Lambda表达式中代码出现的异常会跟首个参数的异常类型进行比较,如果不属于同一类异常,则失败
@Test
@DisplayName("Exception Test Demo2")
void assertThrowsException2() {
    String str = null;
    assertThrows(NullPointerException.class, () -> {
        Integer.valueOf(str);
    });
}
2.5:嵌套测试

嵌套测试给测试编写者更多的能力,来表达几组测试之间的关系。

@DisplayName("A stack")
public class NestedTest {

    Stack stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {
        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}
2.6:重复测试

JUnit Jupiter通过使用@RepeatedTest注解方法并指定所需的重复次数,提供了重复测试指定次数的功能。

每次重复测试的调用都像执行常规的@Test方法一样,完全支持相同的生命周期回调和扩展。

以下示例演示了如何声明名为repeatedTest()的测试,该测试将自动重复10次。

@RepeatedTest(10)
void repeatedTest() {
    // ...
}

除了指定重复次数外,还可以通过@RepeatedTest注解的name属性为每次重复配置自定义显示名称。

此外,显示名称可以是模式,由静态文本和动态占位符的组合而成。目前支持以下占位符:

  • {displayName}: @RepeatedTest方法的显示名称
  • {currentRepetition}: 当前重复次数
  • {totalRepetitions}: 重复的总次数
RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
    @DisplayName("Repeat!")
    void customDisplayName(TestInfo testInfo) {
    assertEquals(testInfo.getDisplayName(), "Repeat! 1/1");
}

@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
    assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1");
}
2.7:参数化测试

JUnit Jupiter开箱即用,提供了不少source注解。

具体请参阅org.junit.jupiter.params.provider包中的JavaDoc以获取更多信息。

@ValueSource

最简单的source之一。它可以让你指定一个原生类型(String,int,long或double)的数组,并且只能为每次调用提供一个参数。

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertNotNull(argument);
}

@EnumSource

提供了一个使用Enum常量的简便方法。该注释提供了一个可选的name参数,可以指定使用哪些常量。如果省略,所有的常量将被使用

@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnumSource(TimeUnit timeUnit) {
    assertNotNull(timeUnit);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
    assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}

@EnumSource注解还提供了一个可选的mode参数,可以对将哪些常量传递给测试方法进行细化控制。

例如,您可以从枚举常量池中排除名称或指定正则表达式,如下例所示。

@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
void testWithEnumSourceExclude(TimeUnit timeUnit) {
    assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
    assertTrue(timeUnit.name().length() > 5);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$")
void testWithEnumSourceRegex(TimeUnit timeUnit) {
    String name = timeUnit.name();
    assertTrue(name.startsWith("M") || name.startsWith("N"));
    assertTrue(name.endsWith("SECONDS"));
}

@MethodSource

  • 允许你引用一个或多个测试类的工厂方法。
  • 这样的方法必须返回一个Stream,Iterable,Iterator或者参数数组。另外,这种方法不能接受任何参数。
  • 默认情况下,除非测试类用@TestInstance(Lifecycle.PER_CLASS)注解,否则这些方法必须是静态的。
  • 如果只需要一个参数,则可以返回参数类型的实例Stream,如以下示例所示。
@ParameterizedTest
@MethodSource("stringProvider")
void testWithSimpleMethodSource(String argument) {
    assertNotNull(argument);
}
static Stream<String> stringProvider() {
    return Stream.of("foo", "bar");
}

支持原始类型(DoubleStream,IntStream和LongStream)的流,示例如下:

@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
    assertNotEquals(9, argument);
}
static IntStream range() {
    return IntStream.range(0, 20).skip(10);
}

如果测试方法声明多个参数,则需要返回一个集合或Arguments实例流,如下所示。请注意,Arguments.of(Object…)是Arguments接口中定义的静态工厂方法。

@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
    assertEquals(3, str.length());
    assertTrue(num >=1 && num <=2);
    assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
    return Stream.of(
        Arguments.of("foo", 1, Arrays.asList("a", "b")),
        Arguments.of("bar", 2, Arrays.asList("x", "y"))
    );
}

@CsvSource

@CsvSource允许您将参数列表表示为以逗号分隔的值(例如,字符串文字)。

@ParameterizedTest
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCsvSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}

@CsvSource使用’作为转义字符。 请参阅上述示例和下表中的’baz, qux’值。 一个空的引用值’'会导致一个空的String; 而一个完全空的值被解释为一个null引用。如果null引用的目标类型是基本类型,则引发ArgumentConversionException。

示例输入结果字符列表
@CsvSource({ “foo, bar” })“foo”, “bar”
@CsvSource({ “foo, ‘baz, qux’” })“foo”, “baz, qux”
@CsvSource({ “foo, ‘’” })“foo”, “”
@CsvSource({ “foo, “ })“foo”, null

@CsvFileSource

@CsvFileSource让你使用classpath中的CSV文件。CSV文件中的每一行都会导致参数化测试的一次调用。

@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv")
void testWithCsvFileSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}

two-column.csv

foo, 1
bar, 2
"baz, qux", 3

与@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为转义字符,请参阅上面例子中的"baz, qux"值,一个空的转义值""会产生一个空字符串, 一个完全为空的值被解释为null引用,如果null引用的目标类型是基本类型,则引发ArgumentConversionException。

@ArgumentsSource

可以使用@ArgumentsSource指定一个自定义的,可重用的ArgumentsProvider。

@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
    assertNotNull(argument);
}
static class MyArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream< ? extends Arguments > provideArguments(ExtensionContext context) {
        return Stream.of("foo", "bar").map(Arguments::of);
    }
}
2.8:动态测试

除了这些标准测试外,JUnit Jupiter还引入了一种全新的测试编程模型。这种新的测试是动态测试,它是由 @TestFactory 注解的工厂方法在运行时生成的。

与@Test方法相比,@TestFactory方法本身不是测试用例,而是测试用例的工厂。

因此,动态测试是工厂的产物。

从技术上讲,@TestFactory方法必须返回DynamicNode实例的Stream,Collection,Iterable或Iterator。

DynamicNode的可实例化的子类是DynamicContainer和DynamicTest。

DynamicContainer实例由一个显示名称和一个动态子节点列表组成,可以创建任意嵌套的动态节点层次结构。

然后,DynamicTest实例将被延迟执行,从而实现测试用例的动态甚至非确定性生成。

任何由@TestFactory返回的Stream都要通过调用stream.close()来正确关闭,使得使用诸如Files.lines()之类的资源变得安全。

与@Test方法一样,@TestFactory方法不能是private或static,并且可以选择声明参数,以便通过ParameterResolvers解析。

DynamicTest是运行时生成的测试用例。它由显示名称和Executable组成。

Executable是@FunctionalInterface,这意味着动态测试的实现可以作为lambda表达式或方法引用来提供。

public class DynamicsTest {

    // This will result in a JUnitException!
    @TestFactory
    List<String> dynamicTestsWithInvalidReturnType() {
        return Arrays.asList("Hello");
    }

    @TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
            dynamicTest("1st dynamic test", () -> assertTrue(true)),
            dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }

    @TestFactory
    Iterable<DynamicTest> dynamicTestsFromIterable() {
        return Arrays.asList(
            dynamicTest("3rd dynamic test", () -> assertTrue(true)),
            dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }

    @TestFactory
    Iterator<DynamicTest> dynamicTestsFromIterator() {
        return Arrays.asList(
            dynamicTest("5th dynamic test", () -> assertTrue(true)),
            dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))
        ).iterator();
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("A", "B", "C")
            .map(str -> dynamicTest("test" + str, () -> { /* ... */ }));
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromIntStream() {
        // Generates tests for the first 10 even integers.
        return IntStream.iterate(0, n -> n + 2).limit(10)
            .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
    }

    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTests() {
        // Generates random positive integers between 0 and 100 until
        // a number evenly divisible by 7 is encountered.
        Iterator<Integer> inputGenerator = new Iterator<Integer>() {
            Random random = new Random();
            int current;

            @Override
            public boolean hasNext() {
                current = random.nextInt(100);
                return current % 7 != 0;
            }

            @Override
            public Integer next() {
                return current;
            }
        };
        // Generates display names like: input:5, input:37, input:85, etc.
        Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
        // Executes tests based on the current input value.
        ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
        // Returns a stream of dynamic tests.
        return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
    }

    @TestFactory
    Stream<DynamicNode> dynamicTestsWithContainers() {
        return Stream.of("A", "B", "C")
            .map(input -> dynamicContainer("Container " + input, Stream.of(
                dynamicTest("not null", () -> assertNotNull(input)),
                dynamicContainer("properties", Stream.of(
                    dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                    dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
                ))
            )));
    }
}

四:Mockito

Mock通常是指,在测试一个对象A时,我们构造一些假的对象来模拟与A之间的交互,而这些Mock对象的行为是我们事先设定且符合预期。

通过这些Mock对象来测试A在正常逻辑,异常逻辑或压力情况下工作是否正常。

而Mockito是最流行的Java mock框架之一

1:什么是Mock测试

Mock通常是指,在测试一个对象A时,我们构造一些假的对象来模拟与A之间的交互,而这些Mock对象的行为是我们事先设定且符合预期

通过这些Mock对象来测试A在正常逻辑,异常逻辑或压力情况下工作是否正常。

Mock 测试就是在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。

Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为

在这里插入图片描述
从上图可以看出如果我们要对A进行测试,那么就要先把整个依赖树构建出来,也就是BCDE的实例。

在这里插入图片描述
从图中可以清晰的看出, mock对象就是在调试期间用来作为真实对象的替代品。

mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试

2:Mock 适用在什么场景

在使用Mock的过程中,发现Mock是有一些通用性的,对于一些应用场景,是非常适合使用Mock的:

  • 真实对象具有不可确定的行为(产生不可预测的结果,如股票的行情)
  • 真实对象很难被创建(比如具体的web容器)
  • 真实对象的某些行为很难触发(比如网络错误)
  • 真实情况令程序的运行速度很慢
  • 真实对象有用户界面
  • 测试需要询问真实对象它是如何被调用的(比如测试可能需要验证某个回调函数是否被调用了)
  • 真实对象实际上并不存在(当需要和其他开发小组,或者新的硬件系统打交道的时候,这是一个普遍的问题)

当然,也有一些不得不Mock的场景:

  • 一些比较难构造的Object:这类Object通常有很多依赖,在单元测试中构造出这样类通常花费的成本太大。
  • 执行操作的时间较长Object:有一些Object的操作费时,而被测对象依赖于这一个操作的执行结果,例如大文件写操作,数据的更新等等
  • 异常逻辑:一些异常的逻辑往往在正常测试中是很难触发的,通过Mock可以人为的控制触发异常逻辑。

3:Mockito

Mockito是最流行的Java mock框架之一:https://site.mockito.org/

PowerMockito Github:https://github.com/powermock/powermock/

概念描述对应类/方法
Mock 对象模拟真实对象的替身mock()
Spy 对象部分真实部分模拟的对象spy()
Stubbing定义模拟对象的行为when().thenReturn()
Verification验证交互行为verify()
Argument Matchers参数匹配器any(), eq()
BDD 风格Given-When-Then 结构given().willReturn()
3.1:基本使用
<!--mockito依赖-->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.7.19</version>
    <scope>test</scope>
</dependency>
<!-- junit依赖 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

  • Mockito:简单轻量级的做mocking测试的框架;
  • mock对象:在调试期间用来作为真实对象的替代品;
  • mock测试:在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试;
  • stub:打桩,就是为mock对象的方法指定返回值(可抛出异常);
  • verify:行为验证,验证指定方法调用情况(是否被调用,调用次数等);
package com.example.docdemo;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

@SpringBootTest
class DocDemoApplicationTests {

    @Test
    void contextLoads() {
    }

    @Test
    void test() {
        // 创建一个mock对象,模拟当前方法依赖的对象
        final List mock = mock(List.class);

        // 使用mock对象(mock对象会对接口或类的方法给出默认实现)
        System.out.println("mock add result is: " + mock.add("hello")); // false
        System.out.println("mock get result is: " + mock.get(0)); //  null
        System.out.println("mock size result is: " + mock.size()); // 0

        // 打桩操作, 规定调用指定的方法时返回的指定结果
        when(mock.get(0)).thenReturn("world"); // 设置当访问0索引时返回world
        System.out.println("mock get result is: " + mock.get(0)); // world
        when(mock.size()).thenReturn(1);
        System.out.println("mock size result is: " + mock.size()); // 1

        //4、验证交互 verification(行为测试:验证方法调用情况)
        verify(mock, times(2)).get(Mockito.anyInt()); // 验证get方法调用2次
        verify(mock, times(2)).size(); // 验证size方法调用2次

        //5、验证返回的结果(这是JUnit的功能)
        assertEquals("second", mock.get(0));
        assertEquals(66, mock.size());
    }

}
3.2:行为验证

一旦mock对象被创建了,mock对象会记住所有的交互,然后你就可以选择性的验证你感兴趣的交互,验证不通过则抛出异常

@Test
public void test1() {
    final List mockList = Mockito.mock(List.class);
    mockList.add("mock1");
    mockList.get(0);
    mockList.size();
    mockList.clear();
    // 验证方法被使用(默认1次)
    Mockito.verify(mockList).add("mock1");
    // 验证方法被使用1次
    Mockito.verify(mockList, Mockito.times(1)).get(0);
    // 验证方法至少被使用1次
    Mockito.verify(mockList, Mockito.atLeast(1)).size();
    // 验证方法没有被使用
    Mockito.verify(mockList, Mockito.never()).contains("mock2");
    // 验证方法至多被使用5次
    Mockito.verify(mockList, Mockito.atMost(5)).clear();
    // 指定方法调用超时时间
    Mockito.verify(mockList, timeout(100)).get(0);
    // 指定时间内需要完成的次数
    Mockito.verify(mockList, timeout(200).atLeastOnce()).size();
}
3.3:测试桩

默认情况下,所有的函数都有返回值。mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean;

  • 一旦测试桩函数被调用,该函数将会一致返回固定的值;

  • 对于 static 和 final 方法, Mockito 无法对其 when(…).thenReturn(…) 操作。

@Test
public void test2() {
    //静态导入,减少代码量:import static org.mockito.Mockito.*;
    final ArrayList mockList = mock(ArrayList.class);

    // 设置方法调用返回值
    when(mockList.add("test2")).thenReturn(true);
    doReturn(true).when(mockList).add("test2");
    System.out.println(mockList.add("test2"));  //true

    // 设置方法调用抛出异常
    when(mockList.get(0)).thenThrow(new RuntimeException());
    doThrow(new RuntimeException()).when(mockList).get(0);
    System.out.println(mockList.get(0));    //throw RuntimeException

    // 无返回方法打桩
    doNothing().when(mockList).clear();

    // 为回调做测试桩(对方法返回进行拦截处理)
    final Answer<String> answer = new Answer<String>() {
        @Override
        public String answer(InvocationOnMock invocationOnMock) throws Throwable {
            final List mock = (List) invocationOnMock.getMock();
            return "mock.size result => " + mock.size();
        }
    };
    when(mockList.get(1)).thenAnswer(answer);
    doAnswer(answer).when(mockList).get(1);
    System.out.println(mockList.get(1));    //mock.size result => 0

    // 对同一方法多次打桩,以最后一次为准
    when(mockList.get(2)).thenReturn("test2_1");
    when(mockList.get(2)).thenReturn("test2_2");
    System.out.println(mockList.get(2));    //test2_2
    System.out.println(mockList.get(2));    //test2_2

    // 设置多次调用同类型结果
    when(mockList.get(3)).thenReturn("test2_1", "test2_2");
    when(mockList.get(3)).thenReturn("test2_1").thenReturn("test2_2");
    System.out.println(mockList.get(3));    //test2_1
    System.out.println(mockList.get(3));    //test2_2

    // 为连续调用做测试桩(为同一个函数调用的不同的返回值或异常做测试桩)
    when(mockList.get(4)).thenReturn("test2").thenThrow(new RuntimeException());
    doReturn("test2").doThrow(new RuntimeException()).when(mockList).get(4);
    System.out.println(mockList.get(4));    //test2
    System.out.println(mockList.get(4));    //throw RuntimeException

    // 无打桩方法,返回默认值
    System.out.println(mockList.get(99));    //null
}
3.4:参数匹配器
  • 参数匹配器使验证和测试桩变得更灵活;

  • 为了合理的使用复杂的参数匹配,使用equals()与anyX() 的匹配器会使得测试代码更简洁、简单。有时,会迫使你重构代码以使用equals()匹配或者实现equals()函数来帮助你进行测试;

  • 如果你使用参数匹配器,所有参数都必须由匹配器提供;

  • 支持自定义参数匹配器;

@Test
public void test3() {
    final Map mockMap = mock(Map.class);

    // 正常打桩测试
    when(mockMap.get("key")).thenReturn("value1");
    System.out.println(mockMap.get("key"));     //value1

    // 为灵活起见,可使用参数匹配器
    when(mockMap.get(anyString())).thenReturn("value2");
    System.out.println(mockMap.get(anyString()));   //value2
    System.out.println(mockMap.get("test_key"));    //value2
    System.out.println(mockMap.get(0)); //null

    // 多个入参时,要么都使用参数匹配器,要么都不使用,否则会异常
    when(mockMap.put(anyString(), anyInt())).thenReturn("value3");
    System.out.println(mockMap.put("key3", 3));     //value3
    System.out.println(mockMap.put(anyString(), anyInt()));     //value3
    System.out.println(mockMap.put("key3", anyInt()));    //异常

    // 行为验证时,也支持使用参数匹配器
    verify(mockMap, atLeastOnce()).get(anyString());
    verify(mockMap).put(anyString(), eq(3));

    // 自定义参数匹配器
    final ArgumentMatcher<ArgumentTestRequest> myArgumentMatcher = new ArgumentMatcher<ArgumentTestRequest>() {
        @Override
        public boolean matches(ArgumentTestRequest request) {
            return "name".equals(request.getName()) || "value".equals(request.getValue());
        }
    };
    // 自定义参数匹配器使用
    final ArgumentTestService mock = mock(ArgumentTestService.class);
    when(mock.argumentTestMethod(argThat(myArgumentMatcher))).thenReturn("success");
    doReturn("success").when(mock).argumentTestMethod(argThat(myArgumentMatcher));
    System.out.println(mock.argumentTestMethod(new ArgumentTestRequest("name", "value")));  // success
    System.out.println(mock.argumentTestMethod(new ArgumentTestRequest()));     //null
}

3.5:执行顺序验证
  • 验证执行顺序是非常灵活的-你不需要一个一个的验证所有交互,只需要验证你感兴趣的对象即可;

  • 你可以仅通过那些需要验证顺序的mock对象来创建InOrder对象;

@Test
public void test4() {
    // 验证同一个对象多个方法的执行顺序
    final List mockList = mock(List.class);
    mockList.add("first");
    mockList.add("second");
    final InOrder inOrder = inOrder(mockList);
    inOrder.verify(mockList).add("first");
    inOrder.verify(mockList).add("second");

    // 验证多个对象多个方法的执行顺序
    final List mockList1 = mock(List.class);
    final List mockList2 = mock(List.class);
    mockList1.get(0);
    mockList1.get(1);
    mockList2.get(0);
    mockList1.get(2);
    mockList2.get(1);
    final InOrder inOrder1 = inOrder(mockList1, mockList2);
    inOrder1.verify(mockList1).get(0);
    inOrder1.verify(mockList1).get(2);
    inOrder1.verify(mockList2).get(1);
}
3.6:使用注解简化mock对象创建
// 代替 mock(ArgumentTestService.class) 创建mock对象;
@Mock
private ArgumentTestService argumentTestService;
// 若改注解修饰的对象有成员变量,@Mock定义的mock对象会被自动注入;
@InjectMocks
private MockitoAnnotationServiceImpl mockitoAnnotationService;

@Test
public void test6() {
    // 注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中;
    MockitoAnnotations.initMocks(this);
    when(argumentTestService.argumentTestMethod(new ArgumentTestRequest())).thenReturn("success");
    System.out.println(argumentTestService.argumentTestMethod(new ArgumentTestRequest()));  //success
    System.out.println(mockitoAnnotationService.mockitoAnnotationTestMethod()); //null
}
3.7:监控真实对象(部分mock)
  • 可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了;

  • 尽量少使用spy对象,使用时也需要小心形式,例如spy对象可以用来处理遗留代码;

  • stub语法中同样提供了部分mock的方法,可以调用真实的方法;

完全mock:

上文讲的内容是完全mock,即创建的mock对象与真实对象无关,mock对象的方法默认都是基本的实现,返回基本类型。可基于接口、实现类创建mock对象。

部分mock:

所谓部分mock,即创建的mock对象时基于真实对象的,mock对象的方法都是默认使用真实对象的方法,除非stub之后,才会以stub为准。基于实现类创建mock对象,否则在没有stub的情况下,调用真实方法时,会出现异常。

注意点:

Mockito并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互,不要期望从监控对象得到正确的结果。 当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果

@Test
public void test7() {
    // stub部分mock(stub中使用真实调用)。注意:需要mock实现类,否则会有异常
    final StubTestService stubTestService = mock(StubTestServiceImpl.class);
    when(stubTestService.stubTestMethodA("paramA")).thenCallRealMethod();
    doCallRealMethod().when(stubTestService).stubTestMethodB();
    System.out.println(stubTestService.stubTestMethodA("paramA"));  //stubTestMethodA is called, param = paramA
    System.out.println(stubTestService.stubTestMethodB());  //stubTestMethodB is called
    System.out.println(stubTestService.stubTestMethodC());  //null

    // spy部分mock
    final LinkedList<String> linkedList = new LinkedList();
    final LinkedList spy = spy(linkedList);
    spy.add("one");
    spy.add("two");
    doReturn(100).when(spy).size();
    when(spy.get(0)).thenReturn("one_test");
    System.out.println(spy.size()); //100
    System.out.println(spy.get(0)); //one_test
    System.out.println(spy.get(1)); //two

    // spy可以类比AOP。在spy中,由于默认是调用真实方法,所以第二种写法不等价于第一种写法,不推荐这种写法。
    doReturn("two_test").when(spy).get(2);
    when(spy.get(2)).thenReturn("two_test"); //异常 java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
    System.out.println(spy.get(2));   //two_test

    // spy对象只是真实对象的复制,真实对象的改变不会影响spy对象
    final List<String> arrayList = new ArrayList<>();
    final List<String> spy1 = spy(arrayList);
    spy1.add(0, "one");
    System.out.println(spy1.get(0));    //one
    arrayList.add(0, "list1");
    System.out.println(arrayList.get(0));   //list1
    System.out.println(spy1.get(0));    //one

    // 若对某个方法stub之后,又想调用真实的方法,可以使用reset(spy)
    final ArrayList<String> arrayList1 = new ArrayList<>();
    final ArrayList<String> spy2 = spy(arrayList1);
    doReturn(100).when(spy2).size();
    System.out.println(spy2.size());    //100
    reset(spy2);
    System.out.println(spy2.size());    //0
}
3.8:@Mock 和 @Spy的使用
  • @Mock 等价于 Mockito.mock(Object.class);

  • @Spy 等价于 Mockito.spy(obj);

区分是mock对象还是spy对象:
Mockito.mockingDetails(someObject).isMock();
Mockito.mockingDetails(someObject).isSpy();

@Mock
private StubTestService stubTestService;
@Spy
private StubTestServiceImpl stubTestServiceImpl;
@Spy
private StubTestService stubTestServiceImpl1 = new StubTestServiceImpl();
@Test
public void test8() {
    MockitoAnnotations.initMocks(this);
    // mock对象返回默认
    System.out.println(stubTestService.stubTestMethodB());  //null
    // spy对象调用真实方法
    System.out.println(stubTestServiceImpl.stubTestMethodC());  //stubTestMethodC is called
    System.out.println(stubTestServiceImpl1.stubTestMethodA("spy"));  //stubTestMethodA is called, param = spy

    // 区分是mock对象还是spy对象
    System.out.println(mockingDetails(stubTestService).isMock());   //true
    System.out.println(mockingDetails(stubTestService).isSpy());    //false
    System.out.println(mockingDetails(stubTestServiceImpl).isSpy());    //true
}

3.9:ArgumentCaptor(参数捕获器)捕获方法参数进行验证
  • 在某些场景中,不光要对方法的返回值和调用进行验证,同时需要验证一系列交互后所传入方法的参数。那么我们可以用参数捕获器来捕获传入方法的参数进行验证,看它是否符合我们的要求。
@Test
public void test9() {
    List mock = mock(List.class);
    List mock1 = mock(List.class);
    mock.add("John");
    mock1.add("Brian");
    mock1.add("Jim");
    // 获取方法参数
    ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
    verify(mock).add(argument.capture());
    System.out.println(argument.getValue());    //John

    // 多次调用获取最后一次
    ArgumentCaptor argument1 = ArgumentCaptor.forClass(String.class);
    verify(mock1, times(2)).add(argument1.capture());
    System.out.println(argument1.getValue());    //Jim

    // 获取所有调用参数
    System.out.println(argument1.getAllValues());    //[Brian, Jim]
}
3.10:自定义验证失败的信息
@Test
public void test11() {
    final ArrayList arrayList = mock(ArrayList.class);
    arrayList.add("one");
    arrayList.add("two");

    verify(arrayList, description("size()没有调用")).size();
    // org.mockito.exceptions.base.MockitoAssertionError: size()没有调用

    verify(arrayList, timeout(200).times(3).description("验证失败")).add(anyString());
    //org.mockito.exceptions.base.MockitoAssertionError: 验证失败
}

3.11:修改没有测试桩的调用的默认返回值
  • 可以指定策略来创建mock对象的返回值。这是一个高级特性,通常来说,你不需要写这样的测试;

  • 它对于遗留系统来说是很有用处的。当你不需要为函数调用打桩时你可以指定一个默认的answer;

@Test
public void test12(){
    // 创建mock对象、使用默认返回
    final ArrayList mockList = mock(ArrayList.class);
    System.out.println(mockList.get(0));    //null

    // 这个实现首先尝试全局配置,如果没有全局配置就会使用默认的回答,它返回0,空集合,null,等等。
    // 参考返回配置:ReturnsEmptyValues
    mock(ArrayList.class, Answers.RETURNS_DEFAULTS);

    // ReturnsSmartNulls首先尝试返回普通值(0,空集合,空字符串,等等)然后它试图返回SmartNull。
    // 如果最终返回对象,那么会简单返回null。一般用在处理遗留代码。
    // 参考返回配置:ReturnsMoreEmptyValues
    mock(ArrayList.class, Answers.RETURNS_SMART_NULLS);

    // 未stub的方法,会调用真实方法。
    //    注1:存根部分模拟使用时(mock.getSomething ()) .thenReturn (fakeValue)语法将调用的方法。对于部分模拟推荐使用doReturn语法。
    //    注2:如果模拟是序列化反序列化,那么这个Answer将无法理解泛型的元数据。
    mock(ArrayList.class, Answers.CALLS_REAL_METHODS);

    // 深度stub,用于嵌套对象的mock。参考:https://www.cnblogs.com/Ming8006/p/6297333.html
    mock(ArrayList.class, Answers.RETURNS_DEEP_STUBS);

    // ReturnsMocks首先尝试返回普通值(0,空集合,空字符串,等等)然后它试图返回mock。
    // 如果返回类型不能mocked(例如是final)然后返回null。
    mock(ArrayList.class, Answers.RETURNS_MOCKS);

    //  mock对象的方法调用后,可以返回自己(类似builder模式)
    mock(ArrayList.class, Answers.RETURNS_SELF);

    // 自定义返回
    final Answer<String> answer = new Answer<String>() {
        @Override
        public String answer(InvocationOnMock invocation) throws Throwable {
            return "test_answer";
        }
    };
    final ArrayList mockList1 = mock(ArrayList.class, answer);
    System.out.println(mockList1.get(0));   //test_answer
}

4:真实业务示例

测试实体类如下

@Data
public class User {

    /**
     * 姓名,登录密码
     */
    
}

持久层DAO

public interface UserDao {

    /**
     * 根据name查找user
     * @param name
     * @return
     */
    User getUserByName(String name);

    /**
     * 保存user
     * @param user
     * @return
     */
    Integer saveUser(User user);
}

业务层接口

public interface UserService {

    /**
     * 根据name查找user
     * @param name
     * @return
     */
    User getUserByName(String name);

    /**
     * 保存user
     * @param user
     * @return
     */
    Integer saveUser(User user);
}

@Service
public class UserServiceImpl implements UserService {

    //userDao
    @Autowired
    private UserDao userDao;

    /**
     * 根据name查找user
     * @param name
     * @return
     */
    @Override
    public User getUserByName(String name) {
        try {
            return userDao.getUserByName(name);
        } catch (Exception e) {
            throw new RuntimeException("查询user异常");
        }
    }

    /**
     * 保存user
     * @param user
     * @return
     */
    @Override
    public Integer saveUser(User user) {
        if (userDao.getUserByName(user.getName()) != null) {
            throw new RuntimeException("用户名已存在");
        }
        try {
            return userDao.saveUser(user);
        } catch (Exception e) {
            throw new RuntimeException("保存用户异常");
        }
    }
}

现在我们的Service写好了,想要单元测试一下,但是Dao是其他人开发的,目前还没有写好,那我们如何测试呢?

public class UserServiceTest {

    /**
     * Mock测试:根据name查询user
     */
    @Test
    public void getUserByNameTest() {
        // mock对象
        final UserDao userDao = mock(UserDao.class);
        final UserServiceImpl userService = new UserServiceImpl();
        userService.setUserDao(userDao);

        // stub调用
        final User user = new User();
        user.setName("admin");
        user.setPassword("pass");
        when(userDao.getUserByName("admin")).thenReturn(user);

        // 执行待测试方法
        final User user1 = userService.getUserByName("admin");
        System.out.println("查询结果:" + JacksonUtil.obj2json(user1));  //查询结果:{"name":"admin","password":"pass"}

        // 验证mock对象交互
        verify(userDao).getUserByName(anyString());

        // 验证查询结果
        Assert.assertNotNull("查询结果为空!", user1);
        Assert.assertEquals("查询结果错误!", "admin", user1.getName());
    }


    /**
     * Mock测试:保存user
     */
    @Mock
    private UserDao userDao;
    @InjectMocks
    private UserServiceImpl userService;
    @Test
    public void saveUserTest() throws Exception{
        // 执行注解初始化
        MockitoAnnotations.initMocks(this);

        // mock对象stub操作
        final User user = new User();
        user.setName("admin");
        user.setPassword("pass");
        when(userDao.getUserByName("admin")).thenReturn(user).thenReturn(null);
        when(userDao.saveUser(any(User.class))).thenReturn(1);

        // 验证用户名重复的情况
        try {
            userService.saveUser(user);
            throw new Exception();  //走到这里说明验证失败
        } catch (RuntimeException e) {
            System.out.println("重复用户名保存失败-测试通过");   //重复用户名保存失败-测试通过
        }
        verify(userDao).getUserByName("admin");

        // 验证正常保存的情况
        user.setName("user");
        final Integer integer = userService.saveUser(user);
        System.out.println("保存结果:" + integer);  //保存结果:1
        Assert.assertEquals("保存失败!", 1, integer.longValue());

        verify(userDao).saveUser(any(User.class));
        verify(userDao, times(2)).getUserByName(anyString());
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值