Junit5+Mockito单元测试详解

Junit5+Mockito进行单元测试

单元测试原则:分宏观微观

1.宏观层面:AIR原则
  • A:Automatic(自动化)
    全自动执行,输出结果无需人工检查,而是通过断言验证。
  • I:Independent(独立性)
    分层测试,各层之间不相互依赖。
  • R:Repeatable(可重复)
    可重复执行,不受外部环境( 网络、服务、中间件等)影响。
2.微观层面:BCDE原则

B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
C: Correct,正确的输入,并得到预期的结果。
D: Design,与设计文档相结合,来编写单元测试。
E : Error,单元测试的目标是证明程序有错,而不是程序无错。为了发现代码中潜在的错误, 我们需要在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果。


一.单元测试的概念

1.概念:

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

单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。执行单元测试,就是为了证明这 段代码的行为和我们期望是否一致。

Spring提供了开发者用于单元测试的依赖包:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

该依赖包含以下几个库

  • Junit ——常用的单元测试库
  • Spring Test & Spring Boot Test ——对Spring应用的集成测试支持
  • AssertJ——一个断言库
  • Hamcrest—— 一个匹配对象的库(主要用于校验的 Java 的单元测试框架,可以组合创建灵活的表达的匹配器进行断言)
  • Mockito—— 一个Java模拟框架(对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法)
  • JSONassert—— 一个针对JSON的断言库
  • JsonPath—— 用于JSON的XPath((XML Path Language)用于确定Json位置)

二、单元测试的作用

1.写单元测试的两个动机:

  1. 验证功能的完整性,确保功能的可用性。
  2. 保护已经实现的功能不被破坏。

三、如何进行单元测试

1.Junit的变化

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

JUnit 5官方文档

作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。

img

首先我们需要了解 Junit5

其中Junit5包含 Junit Platform、Junit Jupiter、Junit Vintage

①其中 Junit Platform 是Junit致力于打造跨平台测试而创建的测试平台

(在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。)

②Junit Jupiter 是Junit5的核心所在

(提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。)

③Junit Vintage主要是适配兼容Junit4的功能

(为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎)

注意:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)

如果需要继续兼容junit4需要自行引入vintage

Junit5的基本单元测试模板**(区别于Junit4:Spring的JUnit4的是@SpringBootTest+@RunWith(SpringRunner.class))**

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;//注意不是org.junit.Test(这是JUnit4版本的)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringBootApplicationTests {

    @Autowired
    private Component component;
    
    @Test
    //@Transactional 标注后连接数据库有回滚功能
    public void contextLoads() {
		Assertions.assertEquals(5, component.getFive());
    }
}


SpringBoot整合Junit以后

  • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  • Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚

2.JUnit5常用注解

官方文档 - Annotations

@Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试

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

import org.junit.jupiter.api.*;

@DisplayName("junit5功能测试类")
public class Junit5Test {

	/**
	*@displayName()括号内参数为方法或类别名
	*
	*
	*/
    @DisplayName("测试displayname注解")
    @Test
    void testDisplayName() {
        System.out.println(1);
        System.out.println(jdbcTemplate);
    }
    
     /**
     *  @ValueSource()后面参数化测试会再讲
     *	声明一个源,该源将为每次调用提供参数,然后在测试方法中使用这些参数
     * 	StringUtils.isPalindrome:用于判断是否为回文数
     */
    @ParameterizedTest
    @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
    void palindromes(String candidate) {
        assertTrue(StringUtils.isPalindrome(candidate));
    }
    
    
    /**
	*@Disable:表示方法不执行。同Junit4中@Ignore
	*
	*
	*/
    @Disabled
    @DisplayName("测试方法2")
    @Test
    void test2() {
        System.out.println(2);
    }
    /**
	*@RepeatedTest():修饰在方法上,表示会自动重复测试这个方法,比如@RepeatedTest(10),会自动执行10次
	*
	*
	*/
    @RepeatedTest(5)
    @Test
    void test3() {
        System.out.println(5);
    }

    /**
     * 规定方法超时时间。超出时间测试出异常
     *
     * @throws InterruptedException
     */
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
    @Test
    void testTimeout() throws InterruptedException {
        Thread.sleep(600);
    }
     /**
	*@BeforeEach:修饰在方法上,在每一个测试方法(所有@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方	   * 法)之前执行一次
	*例如:一个测试类有2个测试方法testA()和testB(),还有一个@BeforeEach的方法,执行这个测试类,@BeforeEach的方法会在		     * testA()之前执行一次,在testB()之前又执行一次。@BeforeEach的方法一共执行了2次。
	*
	*/
    @BeforeEach
    void testBeforeEach() {
        System.out.println("测试就要开始了...");
    }
    /**
	*@AfterEach:修饰在方法上,和@BeforeEach对应,在每一个测试方法(所有@Test、@RepeatedTest、@ParameterizedTest或	     *	者@TestFactory注解的方法)之后执行一次。
	*
	*
	*/
    @AfterEach
    void testAfterEach() {
        System.out.println("测试结束了...");
    }
    /**
	*@BeforeAll:修饰在方法上,使用该注解的方法在当前整个测试类中所有的测试方法之前执行,每个测试类运行时只会执行一次。
	*
	*
	*/
    @BeforeAll
    static void testBeforeAll() {
        System.out.println("所有测试就要开始了...");
    }
    /**
	*修饰在方法上,与@BeforeAll对应,使用该注解的方法在当前测试类中所有测试方法都执行完毕后执行的,每个测试类运行时只会执行一	     *次。
	*
	*
	*/
    @AfterAll
    static void testAfterAll() {
        System.out.println("所有测试以及结束了...");

    }

}

3.断言(通俗的讲判断程序运行是否符合预期)

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:

检查业务逻辑返回的数据是否合理。

所有的测试运行结束以后,会有一个详细的测试报告;

JUnit 5 内置的断言可以分成如下几个类别:

①.简单断言

用来对单个值进行简单的验证。如:

方法说明
assertEquals判断两个对象或两个原始类型是否相等
assertNotEquals判断两个对象或两个原始类型是否不相等
assertSame判断两个对象引用是否指向同一个对象
assertNotSame判断两个对象引用是否指向不同的对象
assertTrue判断给定的布尔值是否为 true
assertFalse判断给定的布尔值是否为 false
assertNull判断给定的对象引用是否为 null
assertNotNull判断给定的对象引用是否不为 null
@Test
@DisplayName("simple assertion")
public void simple() {
     assertEquals(3, 1 + 2, "simple math");
     assertNotEquals(3, 1 + 1);

     assertNotSame(new Object(), new Object());
     Object obj = new Object();
     assertSame(obj, obj);

     assertFalse(1 > 2);
     assertTrue(1 < 2);

     assertNull(null);
     assertNotNull(new Object());
}

②.数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

@Test
@DisplayName("array assertion")
public void array() {
	assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

③.组合断言

assertAll()方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。

@Test
@DisplayName("assert all")
public void all() {
 assertAll("Math",
    () -> assertEquals(2, 1 + 1),
    () -> assertTrue(1 > 0)
 );
}

④.异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows(),配合函数式编程就可以进行使用。

@Test
@DisplayName("异常测试")
public void exceptionTest() {
    ArithmeticException exception = Assertions.assertThrows(
           //扔出断言异常
            ArithmeticException.class, () -> System.out.println(1 % 0));
}

⑤.超时断言

JUnit5还提供了Assertions.assertTimeout()为测试方法设置了超时时间。

@Test
@DisplayName("超时测试")
public void timeoutTest() {
    //如果测试方法时间超过1s将会异常
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}

⑥.快速失败

通过 fail 方法直接使得测试失败。

@Test
@DisplayName("fail")
public void shouldFail() {
	fail("This should fail");
}

4.前置条件

JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。就跟@Ignore注解实现效果一样

前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

@DisplayName("前置条件")
public class AssumptionsTest {
    private final String environment = "DEV";

    @Test
    @DisplayName("simple")
    public void simpleAssume() {
        assumeTrue(Objects.equals(this.environment, "DEV"));
        assumeFalse(() -> Objects.equals(this.environment, "PROD"));
    }

    @Test
    @DisplayName("assume then do")
    public void assumeThenDo() {
        assumingThat(
            Objects.equals(this.environment, "DEV"),
            () -> System.out.println("In DEV")
        );
    }
}

assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。

assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

5.嵌套测试

官方文档 - Nested Tests

什么是嵌套测试:项目包含多个模块,模块下又包含多个功能,也就是层层嵌套的,嵌套测试能表现出 层级关系,例如测试用例管理

Unit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> 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());
            }
        }
    }
}


6.参数化测试

官方文档 - Parameterized Tests

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  • @NullSource: 表示为参数化测试提供一个null的入参
  • @EnumSource: 表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

参数化测试的七种方式

①:@ValueSource

@ValueSource是最简单的参数化方式,它是一个数组,支持以下数据类型:

  • short
  • byte
  • int
  • long
  • float
  • double
  • char
  • boolean
  • java.lang.String
  • java.lang.Class
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertTrue(argument > 0 && argument < 4);
}
②:@Null and Empty Sources

表示为参数化测试提供一个null的入参

 	@ParameterizedTest
    @DisplayName("测试NULL参数")
    @NullSource
    void test(Object i){
        if (i==null){
            System.out.println(1);
        }else{
            System.out.println(2);
        }
    }

③:@EnumSource: 表示为参数化测试提供一个枚举入参
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
    assertNotNull(unit);
}

其中的ChronoUnit是个日期枚举类。

ChronoUnit是接口TemporalUnit的实现类,如果测试方法的参数为TemporalUnit,那么需要给@EnumSource加上值:

@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
    assertNotNull(unit);
}

因为JUnit5规定了@EnumSource的默认值的类型必须是枚举类型。

names属性用来指定使用哪些特定的枚举值:

@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
    assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}

mode属性用来指定使用模式,比如排除哪些枚举值:

@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
    assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}

采用正则进行匹配

@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
    assertTrue(unit.name().endsWith("DAYS"));
}
④:@MethodSource

参数值为factory方法,并且factory方法不能带参数。

@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
    assertNotNull(argument);
}
 
static Stream<String> stringProvider() {
    return Stream.of("apple", "banana");
}

除非是@TestInstance(Lifecycle.PER_CLASS)生命周期,否则factory方法必须是static。factory方法的返回值是能转换为Stream的类型,比如Stream, DoubleStream, LongStream, IntStream, Collection, Iterator, Iterable, 对象数组, 或者基元类型数组,比如:

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

@MethodSource的属性如果省略了,那么JUnit Jupiter会找跟测试方法同名的factory方法

如果测试方法有多个参数,那么factory方法也应该返回多个:

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

参数化的值为csv格式的数据(默认逗号分隔),比如:

@ParameterizedTest
@CsvSource({
    "apple,         1",
    "banana,        2",
    "'lemon, lime', 0xF1"
})
void testWithCsvSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertNotEquals(0, rank);
}

delimiter属性可以设置分隔字符。delimiterString属性可以设置分隔字符串(String而非char)。

更多输入输出示例如下:

image-20210714140242605

注意,如果null引用的目标类型是基元类型,那么会报异常ArgumentConversionException

⑥:@CsvFileSource

顾名思义,选择本地csv文件作为数据来源。

JUnit只在classpath中查找指定的CSV文件,因此,test-capitalize.csv这个文件要放到test目录下,内容如下

@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromClasspath(String country, int reference) {
    assertNotNull(country);
    assertNotEquals(0, reference);
}
 
@ParameterizedTest
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
    assertNotNull(country);
    assertNotEquals(0, reference);
}

delimiter属性可以设置分隔字符。delimiterString属性可以设置分隔字符串(String而非char)。需要特别注意的是,#开头的行会被认为是注释而略过。

⑦:@ArgumentsSource

如果JUnit内置的源代码无法满足您的所有用例,则可以自由创建自己的用例。 必须实现此接口。

public interface ArgumentsProvider {
    Stream<? extends Arguments> provideArguments(
        ContainerExtensionContext context) throws Exception;
}
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
    assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of("apple", "banana").map(Arguments::of);
    }
}

本次主要以Json格式的入参为例:

JsonArgumentsProvider实现了ArgumentsProvider,AnnotationConsumer

package org.testerhome.junit5.json.params.test;

import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.testerhome.junit5.json.params.JsonArgumentsProvider;
import org.testerhome.junit5.json.params.JsonSource;

import java.sql.SQLOutput;

public class JsonArgumentsProviderTest {

    @Test
    @DisplayName("default constructor does not throw")
    void defaultConstructor() {
        Assertions.assertDoesNotThrow(JsonArgumentsProvider::new);
    }

    /**
     * When passed <code>{"key":"value"}</code>, is executed a single time
     * @param object the parsed JsonObject
     */
    @ParameterizedTest
    @JsonSource("{\"key\":\"value\"}")
    @DisplayName(",")
    void singleObject(JSONObject object) {
        System.out.println(object);
        Assertions.assertEquals(object.getString("key"), "value");
    }

    /**
     * When passed <code>[{"key":"value1"},{"key","value2"}]</code>, is
     * executed once per element of the array
     * @param object the parsed JsonObject array element
     */
    @ParameterizedTest
    @JsonSource("[{\"key\":\"value1\"},{\"key\":\"value2\"}]")
    @DisplayName("provides an array of objects")
    void arrayOfObjects(JSONObject object) {
        Assertions.assertTrue(object.getString("key").startsWith("value"));
    }

    /**
     * When passed <code>[1, 2]</code>, is executed once per array element
     * @param number the parsed JsonNumber for each array element
     */
    @ParameterizedTest
    @JsonSource("[1,2]")
    @DisplayName("provides an array of numbers")
    void arrayOfNumbers(Integer number) {
        Assertions.assertTrue(number > 0);
    }

    /**
     * When passed <code>["value1","value2"]</code>, is executed once per array
     * element
     * @param value the parsed JsonString for each array element
     */
    @ParameterizedTest
    @JsonSource("[\"value1\",\"value2\",\"value3\"]")
    @DisplayName("provides an array of strings")
    void arrayOfStrings(String value) {
        Assertions.assertTrue(value.startsWith("value"));
    }

    @ParameterizedTest
    @JsonSource("{'key':'value'}")
    @DisplayName("handles simplified json")
    void simplifiedJson(JSONObject object) {
        Assertions.assertTrue(object.getString("key").equals("value"));
    }
}



json参数格式泛型
package org.testerhome.junit5.json.params;

import org.junit.jupiter.params.provider.ArgumentsSource;

import java.lang.annotation.*;

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ArgumentsSource(JsonArgumentsProvider.class)
public @interface JsonSource {
    String value();
}

JsonFile文件形式作为入参

package org.testerhome.junit5.json.params.test;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.testerhome.junit5.json.params.JsonFileArgumentsProvider;
import org.testerhome.junit5.json.params.JsonFileSource;

public class JsonFileArgumentsProviderTest {

    @Test
    @DisplayName("default constructor does not throw")
    void defaultConstructor() {
        Assertions.assertDoesNotThrow(JsonFileArgumentsProvider::new);
    }

    /**
     * When passed <code>{"key":"value"}</code>, is executed a single time
     *
     * @param object the parsed JsonObject
     */
    @ParameterizedTest
    @JsonFileSource(resources = "/single-object.json")
    @DisplayName("provides a single object")
    void singleObject(JSONObject object) {
        System.out.println(object.getString("key"));
        Assertions.assertEquals(object.getString("key"), "value");
    }

    /**
     * When passed <code>[{"key":"value1"},{"key","value2"}]</code>, is
     * executed once per element of the array
     *
     * @param object the parsed JsonObject array element
     */
    @ParameterizedTest
    @JsonFileSource(resources = "/array-of-objects.json")
    @DisplayName("provides an array of objects")
    void arrayOfObjects(JSONObject object) {
        Assertions.assertTrue(object.getString("key").startsWith("value"));
    }

    /**
     * When passed <code>[1, 2]</code>, is executed once per array element
     *
     * @param number the parsed JsonNumber for each array element
     */
    @ParameterizedTest
    @JsonFileSource(resources = "/array-of-numbers.json")
    @DisplayName("provides an array of numbers")
    void arrayOfNumbers(Integer number) {
        Assertions.assertTrue(number > 0);
    }

    /**
     * When passed <code>["value1","value2"]</code>, is executed once per array
     * element
     *
     * @param value the parsed JsonString for each array element
     */
    @ParameterizedTest
    @JsonFileSource(resources = "/array-of-strings.json")
    @DisplayName("provides an array of strings")
    void arrayOfStrings(String value) {
        Assertions.assertTrue(value.startsWith("value"));
    }


    /**
     * @param jsonObject
     */
    @ParameterizedTest
    @Order(1)
    @JsonFileSource(resources = "/complex.json")
    @DisplayName("provides complex json case")
    void complexJson(JSONObject jsonObject) {
        JSONArray topics = jsonObject.getJSONArray("topics");
        for (Object t : topics) {
            String title = ((JSONObject) t).getString("title");
            System.out.println(title);
        }
    }
}


json文件格式泛型
package org.testerhome.junit5.json.params;

import org.junit.jupiter.params.provider.ArgumentsSource;

import java.lang.annotation.*;

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ArgumentsSource(JsonFileArgumentsProvider.class)
public @interface JsonFileSource {
    String[] resources();
}

测试示例代码

在这里插入图片描述

//以json参数为例
package org.testerhome.junit5.json.params.test;

import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.testerhome.junit5.json.params.JsonArgumentsProvider;
import org.testerhome.junit5.json.params.JsonSource;

import java.sql.SQLOutput;

public class JsonArgumentsProviderTest {

    @Test
    @DisplayName("default constructor does not throw")
    void defaultConstructor() {
        Assertions.assertDoesNotThrow(JsonArgumentsProvider::new);
    }

    /**
     * When passed <code>{"key":"value"}</code>, is executed a single time
     * @param object the parsed JsonObject
     */
    @ParameterizedTest
    @JsonSource("{\"key\":\"value\"}")
    @DisplayName(",")
    void singleObject(JSONObject object) {
        System.out.println(object);
        Assertions.assertEquals(object.getString("key"), "value");
    }

    /**
     * When passed <code>[{"key":"value1"},{"key","value2"}]</code>, is
     * executed once per element of the array
     * @param object the parsed JsonObject array element
     */
    @ParameterizedTest
    @JsonSource("[{\"key\":\"value1\"},{\"key\":\"value2\"}]")
    @DisplayName("provides an array of objects")
    void arrayOfObjects(JSONObject object) {
        Assertions.assertTrue(object.getString("key").startsWith("value"));
    }

    /**
     * When passed <code>[1, 2]</code>, is executed once per array element
     * @param number the parsed JsonNumber for each array element
     */
    @ParameterizedTest
    @JsonSource("[1,2]")
    @DisplayName("provides an array of numbers")
    void arrayOfNumbers(Integer number) {
        Assertions.assertTrue(number > 0);
    }

    /**
     * When passed <code>["value1","value2"]</code>, is executed once per array
     * element
     * @param value the parsed JsonString for each array element
     */
    @ParameterizedTest
    @JsonSource("[\"value1\",\"value2\",\"value3\"]")
    @DisplayName("provides an array of strings")
    void arrayOfStrings(String value) {
        Assertions.assertTrue(value.startsWith("value"));
    }

    @ParameterizedTest
    @JsonSource("{'key':'value'}")
    @DisplayName("handles simplified json")
    void simplifiedJson(JSONObject object) {
        Assertions.assertTrue(object.getString("key").equals("value"));
    }
}

//以参数文件为例
package org.testerhome.junit5.json.params.test;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.testerhome.junit5.json.params.JsonFileArgumentsProvider;
import org.testerhome.junit5.json.params.JsonFileSource;

public class JsonFileArgumentsProviderTest {

    @Test
    @DisplayName("default constructor does not throw")
    void defaultConstructor() {
        Assertions.assertDoesNotThrow(JsonFileArgumentsProvider::new);
    }

    /**
     * When passed <code>{"key":"value"}</code>, is executed a single time
     *
     * @param object the parsed JsonObject
     */
    @ParameterizedTest
    @JsonFileSource(resources = "/single-object.json")
    @DisplayName("provides a single object")
    void singleObject(JSONObject object) {
        System.out.println(object.getString("key"));
        Assertions.assertEquals(object.getString("key"), "value");
    }

    /**
     * When passed <code>[{"key":"value1"},{"key","value2"}]</code>, is
     * executed once per element of the array
     *
     * @param object the parsed JsonObject array element
     */
    @ParameterizedTest
    @JsonFileSource(resources = "/array-of-objects.json")
    @DisplayName("provides an array of objects")
    void arrayOfObjects(JSONObject object) {
        Assertions.assertTrue(object.getString("key").startsWith("value"));
    }

    /**
     * When passed <code>[1, 2]</code>, is executed once per array element
     *
     * @param number the parsed JsonNumber for each array element
     */
    @ParameterizedTest
    @JsonFileSource(resources = "/array-of-numbers.json")
    @DisplayName("provides an array of numbers")
    void arrayOfNumbers(Integer number) {
        Assertions.assertTrue(number > 0);
    }

    /**
     * When passed <code>["value1","value2"]</code>, is executed once per array
     * element
     *
     * @param value the parsed JsonString for each array element
     */
    @ParameterizedTest
    @JsonFileSource(resources = "/array-of-strings.json")
    @DisplayName("provides an array of strings")
    void arrayOfStrings(String value) {
        Assertions.assertTrue(value.startsWith("value"));
    }


    /**
     * @param jsonObject
     */
    @ParameterizedTest
    @Order(1)
    @JsonFileSource(resources = "/complex.json")
    @DisplayName("provides complex json case")
    void complexJson(JSONObject jsonObject) {
        JSONArray topics = jsonObject.getJSONArray("topics");
        for (Object t : topics) {
            String title = ((JSONObject) t).getString("title");
            System.out.println(title);
        }
    }
}

Mockito

1. 为什么要使用 mock

Mock 可以理解为创建一个虚假的对象,或者说模拟出一个对象,在测试环境中用来替换掉真实的对象

以达到我们可以:

  • 验证该对象的某些方法的调用情况,调用了多少次,参数是多少
  • 给这个对象的行为做一个定义,来指定返回结果或者指定特定的动作

大白话:主要是实现分层测试,减少结构之间的依赖,模拟你需要的其他层的数据,就像开发中你没有撰写Dao层代码,但是你想测试Service逻辑是否正确,那么你可以Mock(虚构)出Dao层的数据。

2. Mockito 中常用方法

2.1 Mock 方法

mock 方法来自 org.mockito.Mock,它表示可以 mock 一个对象或者是接口。

public static <T> T mock(Class<T> classToMock)
  • classToMock:待 mock 对象的 class 类。
  • 返回 mock 出来的类

实例:使用 mock 方法 mock 一个类

Random random = Mockito.mock(Random.class);

大白话:我在Controller层要调用Service方法,但是我没写,可以使用Service MyService = Mockito.mock(xxxService.class);

2.2给 Mock 对象打桩

打桩可以理解为 mock 对象规定一行的行为,使其按照我们的要求来执行具体的操作。在 Mockito 中,常用的打桩方法为

方法含义
when().thenReturn()Mock 对象在触发指定行为后返回指定值
when().thenThrow()Mock 对象在触发指定行为后抛出指定异常
when().doCallRealMethod()Mock 对象在触发指定行为后调用真实的方法

大白话:我在Controller层需要Service返回某个什么东西比如某个数据,我就new一个数据,然后对Service进行打桩,指定这个service调用这个方法的时候返回我new的数据。

thenReturn() 代码示例

@Test
void check() {
    Random random = Mockito.mock(Random.class, "test");
    Mockito.when(random.nextInt()).thenReturn(100);
    //我希望我在调用random.nexInt()方法时,给我返回100,所以我对他进行打桩
    Assertions.assertEquals(100, random.nextInt());
    
}
测试通过

注意:Mock后不对Mock出的对象进行打桩,那么Mock出的对象就是默认值。

2.3 Mock 静态方法

在Mockito 3.4.0后Mock实现了了模拟静态方法。

首先要引入 Mockito-Inline 的依赖。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>4.3.1</version>
    <scope>test</scope>
</dependency>

使用 mockStatic() 方法来 mock静态方法的所属类,此方法返回一个具有作用域的模拟对象。

@Test
void range() {
    MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class);
    utilities.when(() -> StaticUtils.range(2, 6)).thenReturn(Arrays.asList(10, 11, 12));
    Assertions.assertTrue(StaticUtils.range(2, 6).contains(10));
}
@Test
void name() {
    MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class);
    utilities.when(StaticUtils::name).thenReturn("bilibili");
    Assertions.assertEquals("1", StaticUtils.	name());
}

执行整个测试类后会报错:

org.mockito.exceptions.base.MockitoException: 
For com.echo.mockito.Util.StaticUtils, static mocking is already registered in the current thread

To create a new mock, the existing static mock registration must be deregistered

原因是因为 mockStatic() 方法是将当前需要 mock 的类注册到本地线程上(ThreadLocal),而这个注册在一次 mock 使用完之后是不会消失的,需要我们手动的去销毁。如过没有销毁,再次 mock 这个类的时候 Mockito 将会提示我们 :”当前对象 mock 的对象已经在线程中注册了,请先撤销注册后再试“。这样做的目的也是为了保证模拟出来的对象之间是相互隔离的,保证同时和连续的测试不会收到上下文的影响。

因此我们修改代码:

class StaticUtilsTest {

    @Test
    void range() {
        try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
            utilities.when(() -> StaticUtils.range(2, 6)).thenReturn(Arrays.asList(10, 11, 12));
            Assertions.assertTrue(StaticUtils.range(2, 6).contains(10));
        }

    }

    @Test
    void name() {
        try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
            utilities.when(StaticUtils::name).thenReturn("bilibili");
            Assertions.assertEquals("bilibili", StaticUtils.name());
        }
    }
}

注:try()括号内流等对象会在程序运行结束后默认关闭。

3. Mockito 中常用注解

3.1 可以代替 Mock 方法的 @Mock 注解

Shorthand for mocks creation - @Mock annotation

Important! This needs to be somewhere in the base class or a test runner:

快速 mock 的方法,使用 @mock 注解。

mock 注解需要搭配 MockitoAnnotations.openMocks(testClass) 方法一起使用。

依据不同版本可能还会出现initMocks()未过期的情况。

@Mock
private Random random;

@Test
void check() {
    MockitoAnnotations.openMocks(this);
    Mockito.when(random.nextInt()).thenReturn(100);
    Assertions.assertEquals(100, random.nextInt());
}

3.2 Spy 方法与 @Spy 注解

spy() 方法与 mock() 方法不同的是

  1. 被 spy 的对象会走真实的方法,而 mock 对象不会
  2. spy() 方法的参数是对象实例,mock 的参数是 class

示例:spy 方法与 Mock 方法的对比

@Test
void check() {
    CheckAuthorityImpl checkAuthority = Mockito.spy(new CheckAuthorityImpl());
    int res = checkAuthority.add(1, 2);
    Assertions.assertEquals(3, res);

    CheckAuthorityImpl checkAuthority1 = Mockito.mock(CheckAuthorityImpl.class);
    int res1 = checkAuthority1.add(1, 2);
    Assertions.assertEquals(3, res1);
 }

输出结果

// 第二个 Assertions 断言失败,因为没有给 checkAuthority1 对象打桩,因此返回默认值
org.opentest4j.AssertionFailedError: 
Expected :3
Actual   :0

使用 @Spy 注解代码示例

@Spy
private CheckAuthorityImpl checkAuthority;

@BeforeEach
void setUp() {
    MockitoAnnotations.openMocks(this);
}

@Test
void check() {
    int res = checkAuthority.add(1, 2);
    Assertions.assertEquals(3, res);
}

5.参数匹配

1.精准匹配

import org.junit.Assert;
import org.junit.Test;

import java.util.List;

import static org.mockito.Mockito.*;

public class MockitoDemo {


    @Test
    public void test() {
        List mockList = mock(List.class);

        Assert.assertEquals(0, mockList.size());
        Assert.assertEquals(null, mockList.get(0));

        mockList.add("a");  // 调用 mock 对象的写方法,是没有效果的

        Assert.assertEquals(0, mockList.size());      // 没有指定 size() 方法返回值,这里结果是默认值
        Assert.assertEquals(null, mockList.get(0));   // 没有指定 get(0) 返回值,这里结果是默认值

        when(mockList.get(0)).thenReturn("a");          // 指定 get(0)时返回 a

        Assert.assertEquals(0, mockList.size());        // 没有指定 size() 方法返回值,这里结果是默认值
        Assert.assertEquals("a", mockList.get(0));      // 因为上面指定了 get(0) 返回 a,所以这里会返回 a

        Assert.assertEquals(null, mockList.get(1));     // 没有指定 get(1) 返回值,这里结果是默认值
    }
}

其中when(mockList.get(0)).thenReturn("a"); 指定了get(0)的返回值,这个 0 就是参数的精确匹配。我们还可以让不同的参数对应不同的返回值,例如:

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.List;

import static org.mockito.Mockito.*;

public class MockitoDemo {

    @Mock
    private List<String> mockStringList;

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void test() {

        mockStringList.add("a");

        when(mockStringList.get(0)).thenReturn("a");
        when(mockStringList.get(1)).thenReturn("b");

        Assert.assertEquals("a", mockStringList.get(0));
        Assert.assertEquals("b", mockStringList.get(1));

    }

}

2.模糊匹配

可以使用 Mockito.anyInt() 匹配所有类型为 int 的参数:

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.List;

import static org.mockito.Mockito.*;

public class MockitoDemo {

    @Mock
    private List<String> mockStringList;

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void test() {

        mockStringList.add("a");

        when(mockStringList.get(anyInt())).thenReturn("a");  // 使用 Mockito.anyInt() 匹配所有的 int

        Assert.assertEquals("a", mockStringList.get(0)); 
        Assert.assertEquals("a", mockStringList.get(1));

    }

}

anyInt 只是用来匹配参数的工具之一,目前 mockito 有多种匹配函数,部分如下:

函数名匹配类型
any()所有对象类型
anyInt()基本类型 int、非 null 的 Integer 类型
anyChar()基本类型 char、非 null 的 Character 类型
anyShort()基本类型 short、非 null 的 Short 类型
anyBoolean()基本类型 boolean、非 null 的 Boolean 类型
anyDouble()基本类型 double、非 null 的 Double 类型
anyFloat()基本类型 float、非 null 的 Float 类型
anyLong()基本类型 long、非 null 的 Long 类型
anyByte()基本类型 byte、非 null 的 Byte 类型
anyString()String 类型(不能是 null)
anyList()List<T> 类型(不能是 null)
anyMap()Map<K, V>类型(不能是 null)
anyCollection()Collection<T>类型(不能是 null)
anySet()Set<T>类型(不能是 null)
any(Class<T> type)type类型的对象(不能是 null)
isNull()null
notNull()非 null
isNotNull()非 null

6.Mockito 参数匹配顺序

如果参数匹配即声明了精确匹配,也声明了模糊匹配;又或者同一个值的精确匹配出现了两次,使用时会匹配哪一个?

会匹配符合匹配条件的最新声明的匹配。

例如下面这段代码

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;

import static org.mockito.Mockito.*;

@SpringbootTest
public class MockitoDemo {


    @Mock
    private List<String> testList;

    @Test
    public void test01() {

        // 精确匹配 0
        when(testList.get(0)).thenReturn("a");

        Assert.assertEquals("a", testList.get(0));

        // 精确匹配 0
        when(testList.get(0)).thenReturn("b");

        Assert.assertEquals("b", testList.get(0));

        // 模糊匹配
        when(testList.get(anyInt())).thenReturn("c");

        Assert.assertEquals("c", testList.get(0));
        Assert.assertEquals("c", testList.get(1));

    }

    @Test
    public void test02() {

        // 模糊匹配
        when(testList.get(anyInt())).thenReturn("c");

        Assert.assertEquals("c", testList.get(0));
        Assert.assertEquals("c", testList.get(1));

        // 精确匹配 0
        when(testList.get(0)).thenReturn("a");

        Assert.assertEquals("a", testList.get(0));
        Assert.assertEquals("c", testList.get(1));

    }

}


文章参考
BilibiliUP主:搞钱小王
https://www.bilibili.com/video/BV15S4y1F7Xr/?spm_id_from=333.337.search-card.all.click

乐天笔记:Mockito入门

https://www.letianbiji.com/java-mockito/mockito-parameter-match.html
|

  • 11
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值