Junit5+Mockito单元测试详解

您所在的位置:网站首页 五子棋编写测试点是什么 Junit5+Mockito单元测试详解

Junit5+Mockito单元测试详解

2024-07-12 14:03| 来源: 网络整理| 查看: 265

Junit5+Mockito进行单元测试

文章目录 Junit5+Mockito进行单元测试单元测试原则:分宏观微观1.宏观层面:AIR原则2.微观层面:BCDE原则 一.单元测试的概念1.概念: 二、单元测试的作用1.**写单元测试的两个动机:** 三、如何进行单元测试1.Junit的变化2.JUnit5常用注解3.断言(通俗的讲判断程序运行是否符合预期)①.简单断言②.数组断言③.组合断言④.异常断言⑤.超时断言⑥.快速失败 4.前置条件5.嵌套测试6.参数化测试①:@ValueSource②:@Null and Empty Sources③:@EnumSource: 表示为参数化测试提供一个枚举入参④:@MethodSource⑤ :@CsvSource⑥:@CsvFileSource⑦:@ArgumentsSource Mockito1. 为什么要使用 mock2. Mockito 中常用方法2.1 Mock 方法2.2给 Mock 对象打桩2.3 Mock 静态方法 3. Mockito 中常用注解3.1 可以代替 Mock 方法的 @Mock 注解3.2 Spy 方法与 @Spy 注解 5.参数匹配1.精准匹配2.模糊匹配 6.Mockito 参数匹配顺序

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

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

一.单元测试的概念 1.概念:

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

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

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

org.springframework.boot spring-boot-starter-test test

该依赖包含以下几个库

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.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判断给定的布尔值是否为 trueassertFalse判断给定的布尔值是否为 falseassertNull判断给定的对象引用是否为 nullassertNotNull判断给定的对象引用是否不为 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 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 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是最简单的参数化方式,它是一个数组,支持以下数据类型:

shortbyteintlongfloatdoublecharbooleanjava.lang.Stringjava.lang.Class @ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void testWithValueSource(int argument) { assertTrue(argument > 0 && argument =1 && num 0); } /** * When passed ["value1","value2"], 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 {"key":"value"}, 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 [{"key":"value1"},{"key","value2"}], 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 [1, 2], 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 ["value1","value2"], 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 {"key":"value"}, 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 [{"key":"value1"},{"key","value2"}], 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 [1, 2], 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 ["value1","value2"], 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 {"key":"value"}, 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 [{"key":"value1"},{"key","value2"}], 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 [1, 2], 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 ["value1","value2"], 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 mock(Class 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 的依赖。

org.mockito mockito-inline 4.3.1 test

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

@Test void range() { MockedStatic 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 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 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 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() 方法不同的是

被 spy 的对象会走真实的方法,而 mock 对象不会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 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 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 类型(不能是 null)anyMap()Map类型(不能是 null)anyCollection()Collection类型(不能是 null)anySet()Set类型(不能是 null)any(Class type)type类型的对象(不能是 null)isNull()nullnotNull()非 nullisNotNull()非 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 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 |



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3