網(wǎng)站的原型怎么做百度搜索競(jìng)價(jià)排名
文章目錄
- 單元測(cè)試Mockito
- 1. 入門(mén)
- 1.1 什么是Mockito
- 1.2 優(yōu)勢(shì)
- 1.3 原理
- 2. 使用
- 2.0 環(huán)境準(zhǔn)備
- 2.1 Mock
- 1) Mock對(duì)象創(chuàng)建
- 2) 配置Mock對(duì)象的行為(打樁)
- 3) 驗(yàn)證方法調(diào)用
- 4) 參數(shù)匹配
- 5) 靜態(tài)方法
- 2.2 常用注解
- 1) @Mock
- 2) @BeforeEach 與 @BeforeAfter
- 3) @InjectMocks
- 4) @Spy
- 5) @Captor
- 6) @RunWith和@ExtendWith
- @RunWith
- @ExtendWith
- 2.3 常見(jiàn)區(qū)別
- Mock對(duì)象和Spy對(duì)象區(qū)別
- 3. Springboot 使用
- 3.1 數(shù)據(jù)準(zhǔn)備
- 創(chuàng)建sql
- 引入依賴(lài)
- 添加application.yml
- 編寫(xiě)實(shí)體類(lèi)
- 編寫(xiě)Service層
- 編寫(xiě)controller
- 3.2 測(cè)試
- 1) 創(chuàng)建Mock或者Spy對(duì)象
- 方法一
- 方法二
- 方法三
- 2) 參數(shù)匹配
- 3) 打樁
- 4) 多次打樁
- 5) 實(shí)戰(zhàn)
- 3.3 Springboot測(cè)試注解
- @MockBean
- @SpyBean
單元測(cè)試Mockito
名稱(chēng) | 鏈接 | 備注 |
---|---|---|
mockito英文文檔 | Mockito (Mockito 5.12.0 API) (javadoc.io) | |
mockito中文文檔 | Mockito 中文文檔 ( 2.0.26 beta ) - 《Mockito 框架中文文檔》 - 極客文檔 (geekdaxue.co) | |
視頻教學(xué)鏈接 | https://www.bilibili.com/video/BV1P14y1k7Hi |
1. 入門(mén)
1.1 什么是Mockito
Mockito是Java生態(tài)系統(tǒng)中最受歡迎的單元測(cè)試模擬框架之一,以其簡(jiǎn)潔易用的API和強(qiáng)大的模擬能力贏得了廣大開(kāi)發(fā)者的青睞。Mockito允許我們?cè)诓粚?shí)際依賴(lài)外部資源的情況下對(duì)代碼進(jìn)行徹底且高效的單元測(cè)試,極大地提升了測(cè)試覆蓋率和代碼質(zhì)量。
1.2 優(yōu)勢(shì)
Mockito是一種模擬框架,其核心概念是在測(cè)試過(guò)程中創(chuàng)建并使用“Mock對(duì)象”。Mock對(duì)象是對(duì)實(shí)際對(duì)象的一種模擬,它繼承或?qū)崿F(xiàn)了被測(cè)試類(lèi)所依賴(lài)的接口或類(lèi),但其行為可以根據(jù)測(cè)試需求自由定制??刂破湓跍y(cè)試環(huán)境下的行為,從而將注意力聚焦于類(lèi)本身的邏輯驗(yàn)證上。
- 隔離度高:通過(guò)模擬依賴(lài),減少測(cè)試間的耦合,確保單元測(cè)試真正只關(guān)注被測(cè)試單元的內(nèi)部邏輯。
- 易于使用:API設(shè)計(jì)直觀簡(jiǎn)潔,降低了編寫(xiě)和閱讀測(cè)試用例的難度。
- 詳盡的驗(yàn)證:能夠準(zhǔn)確跟蹤和驗(yàn)證被測(cè)試對(duì)象與其依賴(lài)之間的交互行為。
- 靈活性強(qiáng):支持多種定制模擬行為,無(wú)論是簡(jiǎn)單的返回值還是復(fù)雜的回調(diào)機(jī)制。
- 有利于TDD實(shí)踐:與測(cè)試驅(qū)動(dòng)開(kāi)發(fā)方法論緊密契合,鼓勵(lì)寫(xiě)出更易于測(cè)試的代碼。
1.3 原理
Mockito
的底層原理是使用 cglib
動(dòng)態(tài)生成一個(gè) 代理類(lèi)對(duì)象,因此,mock
出來(lái)的對(duì)象其實(shí)質(zhì)就是一個(gè) 代理,該代理在 沒(méi)有配置/指定行為 的情況下,默認(rèn)返回空值
2. 使用
2.0 環(huán)境準(zhǔn)備
創(chuàng)建一個(gè)普通的maven項(xiàng)目。添加依賴(lài)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.ucarinc.framework</groupId><artifactId>demo1</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><name>demo1</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.13</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>2.0.13</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.10.2</version><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.32</version></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>5.11.0</version><scope>test</scope></dependency></dependencies></project>
2.1 Mock
1) Mock對(duì)象創(chuàng)建
使用Mockito.mock()
方法創(chuàng)建接口或抽象類(lèi)的Mock對(duì)象。下面是它的方法接口
public static <T> T mock(Class<T> classToMock)
- classToMock:待 mock 對(duì)象的 class 類(lèi)。
- 返回 mock 出來(lái)的類(lèi)
實(shí)例:使用 mock 方法 mock 一個(gè)類(lèi)
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import static org.mockito.Mockito.*;public class MyTest {@Testpublic void myTest() {/* 創(chuàng)建 Mock 對(duì)象 */List list = mock(List.class);/* 設(shè)置預(yù)期,當(dāng)調(diào)用 get(0) 方法時(shí)返回 "111" */when(list.get(0)).thenReturn("111");Assert.assertEquals("asd", 1, 1);/* 設(shè)置后返回期望的結(jié)果 */System.out.println(list.get(0));/* 沒(méi)有設(shè)置則返回 null */System.out.println(list.get(1));/* 對(duì) Mock 對(duì)象設(shè)置無(wú)效 */list.add("12");list.add("123");/* 返回之前設(shè)置的結(jié)果 */System.out.println(list.get(0));/* 返回 null */System.out.println(list.get(1));/* size 大小為 0 */System.out.println(list.size());/* 驗(yàn)證操作,驗(yàn)證 get(0) 調(diào)用了 2 次 */verify(list, times(2)).get(0);/* 驗(yàn)證返回結(jié)果 */String ret = (String)list.get(0);Assert.assertEquals(ret, "111");}
}
總結(jié)
junit4 | junit5 | |
---|---|---|
方法一 | @RunWith(MockitojUnitRunner.class)+@Mock等注解 | @ExtendWith(MockitoExtension.class)+@Mock等注解 |
方法二 | Mockito.mock(X.class)MockitoAnnotations.open等靜態(tài)方法 | Mockito.mock(X.class)MockitoAnnotations.open等靜態(tài)方法 |
方法三 | Mocks(this)+@Mock等注解 | Mocks(this)+@Mock等注解 |
2) 配置Mock對(duì)象的行為(打樁)
使用when
和thenReturn
方法配置Mock對(duì)象的行為:
打樁可以理解為mock
對(duì)象規(guī)定一行的行為,使其按照我們的要求來(lái)執(zhí)行具體的操作。在Mockito
中,常用的打樁方法為
方法 | 含義 |
---|---|
when().thenReturn() | Mock 對(duì)象在觸發(fā)指定行為后返回指定值 |
when().thenThrow() | Mock 對(duì)象在觸發(fā)指定行為后拋出指定異常 |
when().doCallRealMethod() | Mock 對(duì)象在觸發(fā)指定行為后調(diào)用真實(shí)的方法 |
thenReturn() 代碼示例
public void test02(){// 模擬random對(duì)象,這個(gè)對(duì)象是假的Random random = Mockito.mock(Random.class);// 當(dāng)調(diào)用了random對(duì)象時(shí),返回100這個(gè)值Mockito.when(random.nextInt()).thenReturn(100);// 驗(yàn)證,應(yīng)該是對(duì)的。有人會(huì)問(wèn),random.nextInt()不是獲取隨機(jī)值嗎?// 現(xiàn)在這個(gè)random對(duì)象是假的Assertions.assertEquals(100, random.nextInt());}
完整的另一個(gè)demo
package com.ucarinc.framework;import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;public class App5Test {private final Logger log= LoggerFactory.getLogger(App5Test.class);public static class MockitoTestController{public int add(int a, int b){System.out.println("測(cè)試了a+b a="+a+",b="+b);return a+b;}}@Testvoid testAdd() {MockitoTestController mockitoTestController = mock(MockitoTestController.class);// 設(shè)置mock對(duì)象的行為(打樁),當(dāng)調(diào)用add(1, 2)時(shí)返回4when(mockitoTestController.add(1, 2)).thenReturn(4);// 調(diào)用mock對(duì)象的方法,返回為4int result = mockitoTestController.add(1, 2);log.info("mockitoTestController.add result={}",result);// 斷言驗(yàn)證:調(diào)用add(1, 2)方法返回值是否為4Assertions.assertEquals(mockitoTestController.add(1, 2),4);// 驗(yàn)證:確保add方法(1, 2)被調(diào)用了一次verify(mockitoTestController,times(2)).add(1, 2);}}
你還可以配置方法拋出異常:
/*** 測(cè)試當(dāng)調(diào)用add方法時(shí)拋出RuntimeException異常的情況。* 該測(cè)試函數(shù)不接受參數(shù),也沒(méi)有返回值。*/@Testvoid testAddException() {TestController mockitoTestController = Mockito.mock(TestController.class);// 設(shè)置mock對(duì)象,在調(diào)用mockitoTestController的add方法時(shí)拋出RuntimeException異常when(mockitoTestController.add(1, 2)).thenThrow(new RuntimeException("add error"));// 驗(yàn)證是否拋出了RuntimeException異常Assertions.assertThrows(RuntimeException.class, () -> mockitoTestController.add(1, 2));}public static class TestController{public int add(int a, int b){System.out.println("測(cè)試了a+b="+a+",b="+b);return a+b;}}
有種特殊情況,就是void返回值打樁
package com.lkcoffee.framework.demo2;/*** @Desciption:* @Author: feixiang.li* @date: 2024-07-12 14:38**/import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;import java.util.List;import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;@ExtendWith(MockitoExtension.class)
public class Test4 {@MockList<String> mockList;@Testpublic void test1(){doNothing().when(mockList).clear();mockList.clear();verify(mockList).clear();}
}
3) 驗(yàn)證方法調(diào)用
Mock對(duì)象進(jìn)行行為驗(yàn)證和結(jié)果斷言。驗(yàn)證是校驗(yàn)對(duì)象是否發(fā)生過(guò)某些行為,Mockito 中驗(yàn)證的方法是:verify
。
常見(jiàn)的驗(yàn)證方法包括:
verify(mock).methodCall()
:驗(yàn)證方法被調(diào)用verify(mock, times(n)).methodCall()
:驗(yàn)證方法被調(diào)用n次verify(mock, never()).methodCall()
:驗(yàn)證方法從未被調(diào)用
驗(yàn)證交換:Verify 配合 time() 方法,可以校驗(yàn)?zāi)承┎僮靼l(fā)生的次數(shù)。
注意:當(dāng)使用 mock 對(duì)象時(shí),如果不對(duì)其行為進(jìn)行定義,則 mock 對(duì)象方法的返回值為返回類(lèi)型的默認(rèn)值。
package com.ucarinc.framework;import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;import java.util.Random;import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;public class AppTest{@Testpublic void test01() {// 使用Mockito模擬一個(gè)Random對(duì)象Random random = Mockito.mock(Random.class);// 調(diào)用nextInt()方法,輸出隨機(jī)數(shù),因random 行為為進(jìn)行打樁,故輸出默認(rèn)值0(random.nextInt() 返回的是int類(lèi)型)System.out.println("第一次:"+random.nextInt());// 驗(yàn)證random.nextInt()這個(gè)方法是否只調(diào)用了一次verify(random).nextInt();// 指定當(dāng)調(diào)用nextInt()時(shí),始終返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次調(diào)用nextInt(),輸出應(yīng)為1// 斷言nextInt()方法返回值是否為1Assertions.assertEquals(1,random.nextInt());// 驗(yàn)證nextInt()方法是否被調(diào)用了兩次verify(random, times(3)).nextInt();}
}
4) 參數(shù)匹配
Mockito提供了多種參數(shù)匹配器(Matchers)用于更靈活的驗(yàn)證和配置行為:
import static org.mockito.ArgumentMatchers.*;when(mockRepository.findById(anyInt())).thenReturn(Optional.of(user));
verify(mockRepository).findById(eq(1));
常見(jiàn)的匹配器包括:
any()
:匹配任何參數(shù)anyInt()
:匹配任何整數(shù)參數(shù)eq(value)
:匹配特定值isNull()
:匹配null值notNull()
:匹配非null值
5) 靜態(tài)方法
添加依賴(lài)
<dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>5.2.0</version><scope>test</scope></dependency><!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.14.0</version></dependency>
如果jdk版本低的話(huà),版本可以低一點(diǎn).
使用 mockStatic() 方法來(lái) mock靜態(tài)方法的所屬類(lèi),此方法返回一個(gè)具有作用域的模擬對(duì)象。
@Testpublic void testJoinWith() {// 使用 Mockito 框架模擬 StringUtils 類(lèi)的靜態(tài)方法MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);// 創(chuàng)建一個(gè)字符串列表,作為 joinWith 方法的輸入?yún)?shù)List<String> stringList = Arrays.asList("a", "b", "c");// 配置模擬行為,當(dāng)調(diào)用 StringUtils.joinWith(",", stringList) 時(shí),返回 "a,b,c"stringUtilsMockedStatic.when(() -> StringUtils.joinWith(",", stringList)).thenReturn("a,b,c");// 斷言驗(yàn)證模擬行為是否正確,即 joinWith 方法返回的字符串是否與預(yù)期的 "a,b,c" 相等Assertions.assertTrue(StringUtils.joinWith(",", stringList).equals("a,b,c"));}
但是如果你寫(xiě)成下面這樣子的話(huà),會(huì)發(fā)送報(bào)錯(cuò)
package com.ucarinc.framework;import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;import java.util.Arrays;
import java.util.List;class Demo2ApplicationTests {@Testpublic void testJoinWith() {// 使用 Mockito 框架模擬 StringUtils 類(lèi)的靜態(tài)方法MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);// 創(chuàng)建一個(gè)字符串列表,作為 joinWith 方法的輸入?yún)?shù)List<String> stringList = Arrays.asList("a", "b", "c");// 配置模擬行為,當(dāng)調(diào)用 StringUtils.joinWith(",", stringList) 時(shí),返回 "a,b,c"stringUtilsMockedStatic.when(() -> StringUtils.joinWith(",", stringList)).thenReturn("a,b,c");// 斷言驗(yàn)證模擬行為是否正確,即 joinWith 方法返回的字符串是否與預(yù)期的 "a,b,c" 相等Assertions.assertTrue(StringUtils.joinWith(",", stringList).equals("a,b,c"));}/*** 測(cè)試StringUtils類(lèi)中的join方法。* 該測(cè)試使用Mockito框架來(lái)模擬靜態(tài)方法的行為,驗(yàn)證join方法是否按照預(yù)期工作。* */@Testpublic void testJoin() {// 使用Mockito模擬StringUtils類(lèi)的靜態(tài)方法MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);// 創(chuàng)建一個(gè)字符串列表作為join方法的輸入List<String> stringList = Arrays.asList("a", "b", "c");// 配置模擬行為,當(dāng)調(diào)用StringUtils.join(",", stringList)時(shí),返回字符串"a,b,c"stringUtilsMockedStatic.when(() -> StringUtils.join(",", stringList)).thenReturn("a,b,c");// 斷言驗(yàn)證模擬行為是否正確,即 join 方法返回的字符串是否與預(yù)期的 "a,b,c" 相等Assertions.assertTrue(StringUtils.join(",", stringList).equals("a,b,c"));}}
然后執(zhí)行整個(gè)測(cè)試類(lèi)后會(huì)報(bào)錯(cuò):,就會(huì)報(bào)錯(cuò)
原因是因?yàn)?mockStatic() 方法是將當(dāng)前需要 mock 的類(lèi)注冊(cè)到本地線程上(ThreadLocal),而這個(gè)注冊(cè)在一次 mock 使用完之后是不會(huì)消失的,需要我們手動(dòng)的去銷(xiāo)毀。如過(guò)沒(méi)有銷(xiāo)毀,再次 mock 這個(gè)類(lèi)的時(shí)候 Mockito 將會(huì)提示我們 :”當(dāng)前對(duì)象 mock 的對(duì)象已經(jīng)在線程中注冊(cè)了,請(qǐng)先撤銷(xiāo)注冊(cè)后再試“。這樣做的目的也是為了保證模擬出來(lái)的對(duì)象之間是相互隔離的,保證同時(shí)和連續(xù)的測(cè)試不會(huì)收到上下文的影響。
2.2 常用注解
1) @Mock
快速 mock
的方法,使用 @mock
注解。
mock 注解需要搭配 MockitoAnnotations.openMocks(testClass)
方法一起使用。
package com.ucarinc.framework;import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;import java.util.Arrays;
import java.util.List;
import java.util.Random;import static org.mockito.Mockito.*;public class App2Test {@Mockprivate Random random;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}/*** 測(cè)試Mockito框架的使用,模擬Random類(lèi)的nextInt方法。* 該測(cè)試函數(shù)沒(méi)有參數(shù)和返回值,主要用于演示Mockito的基本用法。*/@Testpublic void test02() {// 調(diào)用nextInt()方法,輸出隨機(jī)數(shù),因random 行為為進(jìn)行打樁,故輸出默認(rèn)值0(random.nextInt() 返回的是int類(lèi)型)System.out.println("第一次:"+random.nextInt());// 指定當(dāng)調(diào)用nextInt()時(shí),始終返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次調(diào)用nextInt(),輸出應(yīng)為1// 斷言nextInt()方法返回值是否為1Assertions.assertEquals(1,random.nextInt());// 驗(yàn)證nextInt()方法是否被調(diào)用了兩次verify(random, times(3)).nextInt();}}
2) @BeforeEach 與 @BeforeAfter
package com.ucarinc.framework;import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.Random;import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;public class RandomTest02 {private final Logger log= LoggerFactory.getLogger(RandomTest02.class);@Mockprivate Random random;@BeforeEachvoid setUp() {log.info("==============測(cè)試前準(zhǔn)備===============");MockitoAnnotations.openMocks(this);}/*** 測(cè)試Mockito框架的使用,模擬Random類(lèi)的nextInt方法。* 該測(cè)試函數(shù)沒(méi)有參數(shù)和返回值,主要用于演示Mockito的基本用法。*/@Testpublic void test02() {// 調(diào)用nextInt()方法,輸出隨機(jī)數(shù),因random 行為為進(jìn)行打樁,故輸出默認(rèn)值0(random.nextInt() 返回的是int類(lèi)型)System.out.println("第一次:"+random.nextInt());// 指定當(dāng)調(diào)用nextInt()時(shí),始終返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次調(diào)用nextInt(),輸出應(yīng)為1// 斷言nextInt()方法返回值是否為1Assertions.assertEquals(1,random.nextInt());// 驗(yàn)證nextInt()方法是否被調(diào)用了兩次verify(random, times(3)).nextInt();}@AfterEachvoid tearDown() {log.info("==============測(cè)試后結(jié)果===============");}}
3) @InjectMocks
@InjectMocks
用于將模擬對(duì)象注入到被測(cè)試類(lèi)中的相應(yīng)字段。通過(guò)該注解可以自動(dòng)將模擬對(duì)象注入到被測(cè)試類(lèi)中標(biāo)記為@InjectMocks的字段中,可以理解為使用@Mock創(chuàng)建出來(lái)的對(duì)象注入到@InjectMocks創(chuàng)建的對(duì)象中,這樣被測(cè)試類(lèi)就可以使用模擬對(duì)象作為其依賴(lài)了。
package com.ucarinc.framework;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.ArrayList;
import java.util.List;import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;public class App6Test {@MockAClass aClass;@InjectMocksBClass bClass;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}@Testvoid testAdd() {// 當(dāng)調(diào)用a方法時(shí),直接返回1000。a是模擬的when(aClass.add()).thenReturn(1000);Assertions.assertEquals(1003, bClass.add(1,2));}public static class AClass{public AClass(){}public int add(){System.out.println("AClass.add");return 1;}}@Data@AllArgsConstructor@NoArgsConstructorpublic static class BClass {private AClass aClass;public int add(int a, int b) {// 調(diào)用a方法int add = aClass.add();System.out.println("測(cè)試了a+b a=" + a + ",b=" + b + ",add=" + add);return a + b + add;}}}
通常配合@Mock注解一起使用,一般用作service層。然后把mock的mapper層注入其中
@InjectMocks
private UserService userService;@MockBean
private UserMapper userMapper;
4) @Spy
spy()
方法與 mock()
方法不同的是
- 被
spy
的對(duì)象會(huì)走真實(shí)的方法,而mock
對(duì)象不會(huì)spy()
方法的參數(shù)是對(duì)象實(shí)例,mock
的參數(shù)是 class
首先,我們使用mock方法。做一個(gè)測(cè)試
package com.ucarinc.framework;import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;import java.util.Random;import static org.mockito.Mockito.*;public class App3Test {public static class MockitoTestController{public int add(int a, int b){System.out.println("測(cè)試了a+b a="+a+",b="+b);return a+b;}}@Testpublic void test01() {MockitoTestController mockitoTestController =new MockitoTestController();// 調(diào)用實(shí)際的 mockitoTestController 對(duì)象的 add 方法,并驗(yàn)證結(jié)果是否為預(yù)期值int result = mockitoTestController.add(1, 2);Assertions.assertEquals(3, result);// 使用 Mockito 創(chuàng)建 mockitoTest 的 mock 對(duì)象,并對(duì)它調(diào)用 add 方法,然后驗(yàn)證結(jié)果MockitoTestController mockitoTest = Mockito.mock(MockitoTestController.class);int result1 = mockitoTest.add(1, 2);Assertions.assertEquals(3, result1);}}
返回的結(jié)果
第二個(gè) Assertions 斷言失敗,因?yàn)闆](méi)有給 mockitoTest 對(duì)象打樁,因此返回默認(rèn)值
使用@Spy()注解示例。引入依賴(lài)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.ucarinc.framework</groupId><artifactId>demo1</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><name>demo1</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.13</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>2.0.13</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.10.2</version><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.32</version></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>5.11.0</version><scope>test</scope></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>5.2.0</version><scope>test</scope></dependency><!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.14.0</version></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-junit-jupiter</artifactId><version>5.11.0</version><scope>test</scope></dependency></dependencies></project>
代碼測(cè)試
package com.ucarinc.framework;import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;@ExtendWith(MockitoExtension.class)
public class App4Test {private final Logger log= LoggerFactory.getLogger(App4Test.class);public static class MockitoTestController{public int add(int a, int b){System.out.println("測(cè)試了a+b a="+a+",b="+b);return a+b;}}@Spyprivate MockitoTestController mockitoTestController;@BeforeEachvoid setUp() {}/*** 測(cè)試add方法* 該方法模擬調(diào)用mockitoTestController的add方法,傳入?yún)?shù)1和2,期望返回值為3。* 首先,通過(guò)when語(yǔ)句設(shè)置mockitoTestController的add方法返回值為3;* 然后,使用assertThat斷言驗(yàn)證調(diào)用add方法(1, 2)實(shí)際返回值確實(shí)為3;* 最后,通過(guò)verify語(yǔ)句確認(rèn)mockitoTestController的add方法確實(shí)被調(diào)用了一次,并傳入了參數(shù)1和2。*/@Testvoid testAdd() {// 設(shè)置mock對(duì)象的行為(打樁),當(dāng)調(diào)用add(1, 2)時(shí)返回4when(mockitoTestController.add(1, 2)).thenReturn(4);// 調(diào)用mock對(duì)象的方法,返回為4int result = mockitoTestController.add(1, 2);log.info("mockitoTestController.add result={}",result);// 斷言驗(yàn)證:調(diào)用add(1, 2)方法返回值是否為4Assertions.assertEquals(mockitoTestController.add(1, 2),4);// 驗(yàn)證:確保add方法(1, 2)被調(diào)用了一次verify(mockitoTestController,times(2)).add(1, 2);}}
5) @Captor
接下來(lái),我們來(lái)看看如何使用@Captor注解來(lái)創(chuàng)建ArgumentCaptor實(shí)例。
在以下示例中,我們將在不使用@Captor注釋的情況下創(chuàng)建ArgumentCaptor:
@Test
public void whenNotUseCaptorAnnotation_thenCorrect() {List mockList = Mockito.mock(List.class);ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);mockList.add("one");Mockito.verify(mockList).add(arg.capture());assertEquals("one", arg.getValue());
}
使用@Captor來(lái)創(chuàng)建一個(gè)ArgumentCaptor實(shí)例:
@MockList<String> mockedList;@CaptorArgumentCaptor<String> argCaptor;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}@Testpublic void whenUseCaptorAnnotation_thenTheSame() {mockedList.add("one");verify(mockedList).add(argCaptor.capture());assertEquals("one", argCaptor.getValue());}
6) @RunWith和@ExtendWith
測(cè)試類(lèi)上使用 @RunWith(SpringRunner.class) 注解(使用的是 JUnit 4)
測(cè)試類(lèi)上使用 @ExtendWith(SpringExtension.class)注解(使用的是 JUnit 5)SpringBoot2.4.x之后,改為默認(rèn)僅集成JUnit5,干掉了兼容JUnit4
@RunWith
- @RunWith就是一個(gè)運(yùn)行器
- @RunWith(JUnit4.class)就是指用JUnit4來(lái)運(yùn)行
- @RunWith(SpringJUnit4ClassRunner.class),讓測(cè)試運(yùn)行于
Spring
測(cè)試環(huán)境,以便在測(cè)試開(kāi)始的時(shí)候自動(dòng)創(chuàng)建Spring
的應(yīng)用上下文
@RunWith(SpringRunner.class) //14.版本之前用的是SpringJUnit4ClassRunner.class
@SpringBootTest(classes = Application.class) //1.4版本之前用的是//@SpringApplicationConfiguration(classes = Application.class)
public class SystemInfoServiceImplTest {@Autowiredprivate ISystemInfoService systemInfoservice;@Testpublic void add() throws Exception {}@Testpublic void findAll() throws Exception {}}
@ExtendWith
@ExtendWith 具體Demo展示如下:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;// 定義一個(gè)自定義的JUnit擴(kuò)展,用于在測(cè)試開(kāi)始前輸出日志
class CustomExtension implements BeforeTestExecutionCallback {@Overridepublic void beforeTestExecution(ExtensionContext context) {System.out.println("Before Test Execution");}
}// 使用@ExtendWith注解加載自定義擴(kuò)展
@ExtendWith(CustomExtension.class)
public class test {@Testvoid test1() {System.out.println("Test 1");Assertions.assertTrue(true);}@Testvoid test2() {System.out.println("Test 2");Assertions.assertEquals(2, 1 + 1);}
}
Mockito通常與JUnit結(jié)合使用,特別是JUnit 5,利用@ExtendWith(MockitoExtension.class)
簡(jiǎn)化Mock對(duì)象的初始化
啟動(dòng)類(lèi)加上@ExtendWith(MockitoExtension.class),會(huì)自動(dòng)處理@Mock
,@Spy
,@InjectMocks
等注解
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;@ExtendWith(MockitoExtension.class)
public class UserServiceTest {// 測(cè)試代碼
}
2.3 常見(jiàn)區(qū)別
Mock對(duì)象和Spy對(duì)象區(qū)別
方法插樁 | 方法不插樁 | 作用對(duì)象 | 最佳實(shí)踐 | |
---|---|---|---|---|
mock對(duì)象 | 執(zhí)行插樁邏輯 | 返回mock對(duì)象的默認(rèn)值 | 類(lèi)、接口 | 被測(cè)試類(lèi)或其依賴(lài) |
spy對(duì)象 | 執(zhí)行插樁邏輯 | 調(diào)用真實(shí)方法 | 類(lèi)、接口 | 被測(cè)試類(lèi) |
3. Springboot 使用
首先看下完整的pom結(jié)構(gòu)
3.1 數(shù)據(jù)準(zhǔn)備
創(chuàng)建sql
create database if not exists mockito;
use mockito;
DROP TABLE IF EXISTS `user`;CREATE TABLE `user`
(id BIGINT NOT NULL COMMENT '主鍵ID',name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',age INT NULL DEFAULT NULL COMMENT '年齡',email VARCHAR(50) NULL DEFAULT NULL COMMENT '郵箱',PRIMARY KEY (id)
);
INSERT INTO `user` (id, name, age, email)
VALUES (1, 'Jone', 18, 'test1@baomidou.com'),(2, 'Jack', 20, 'test2@baomidou.com'),(3, 'Tom', 28, 'test3@baomidou.com'),(4, 'Sandy', 21, 'test4@baomidou.com'),(5, 'Billie', 24, 'test5@baomidou.com');
引入依賴(lài)
創(chuàng)建springboot 項(xiàng)目。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.1</version><relativePath/> <!-- lookup parent from repository --></parent><modelVersion>4.0.0</modelVersion><groupId>com.lkcoffee.framework</groupId><artifactId>demo2</artifactId><version>0.0.1-SNAPSHOT</version><name>demo2</name><description>demo2</description><properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>3.3.1</spring-boot.version></properties><dependencies><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.7</version></dependency><!-- springbbot配置--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope><version>8.3.0</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.28</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>17</source><target>17</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.lkcoffee.framework.demo2.Demo2Application</mainClass><skip>true</skip></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
添加application.yml
server:port: 8080spring:jackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8servlet:multipart:max-file-size: 1024MBmax-request-size: 1024MBapplication:name: demo2datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/mockito?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: rootmybatis-plus:global-config:db-config:logic-delete-field: isDeletelogic-delete-value: 1logic-not-delete-value: 0mapper-locations: classpath*:mapper/**/*Mapper.xmlconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpllogging:file:name: test.loglevel:root: INFOorg:springframework: DEBUGexample:springboottest: DEBUG
在Springboot 啟動(dòng)類(lèi)中添加 @MapperScan
注解,掃描 Mapper 文件夾:
package com.lkcoffee.framework.demo2;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@MapperScan("com.lkcoffee.framework.demo2.mapper")
@SpringBootApplication
public class Demo2Application {public static void main(String[] args) {SpringApplication.run(Demo2Application.class, args);}}
編寫(xiě)實(shí)體類(lèi)
import lombok.Data;@Data
public class User {private Long id;private String name;private Integer age;private String email;
}
編寫(xiě) Mapper 接口類(lèi) UserMapper.java
:
import org.springframework.stereotype.Repository;@Repository
public interface UserMapper extends BaseMapper<User> {}
編寫(xiě)Service層
package com.lkcoffee.framework.demo2.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.lkcoffee.framework.demo2.domain.User;import java.util.List;/*** @Desciption: 用戶(hù)服務(wù)層* @Author: feixiang.li* @date: 2024-07-11 19:51**/
public interface UserService extends IService<User> {/*** 查詢(xún)所有用戶(hù)信息* @return 所有用戶(hù)信息*/List<User> queryAll();/*** 根據(jù)用戶(hù)id查詢(xún)* @param id 用戶(hù)id* @return 用戶(hù)信息*/User queryById(Long id);/*** 添加用戶(hù)id* @param user 用戶(hù)信息* @return 操作結(jié)果*/Boolean addUser(User user);/*** 根據(jù)用戶(hù)id修改用戶(hù)信息* @param user* @return*/Integer updateUser(User user);
}
實(shí)現(xiàn)Service層
package com.lkcoffee.framework.demo2.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.mapper.UserMapper;
import com.lkcoffee.framework.demo2.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;
import java.util.Objects;/*** @Desciption: 用戶(hù)操作類(lèi)* @Author: feixiang.li* @date: 2024-07-12 10:39**/
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Overridepublic List<User> queryAll() {log.info("被真實(shí)調(diào)用了, 執(zhí)行了 查詢(xún)所有用戶(hù)信息");return list();}@Overridepublic User queryById(Long id) {log.info("被真實(shí)調(diào)用了, 根據(jù)用戶(hù)id:{} 查詢(xún)用戶(hù)",id);return getById(id);}@Transactional(rollbackFor = Exception.class)@Overridepublic Boolean addUser(User user) {log.info("被真實(shí)調(diào)用了, 添加用戶(hù)信息:{}",user);if(Objects.nonNull(user.getId())){throw new RuntimeException("被真實(shí)調(diào)用了,新增用戶(hù),id應(yīng)該為空");}if(Objects.isNull(user.getAge()) || user.getAge() < 0 || user.getAge() > 100){throw new RuntimeException("被真實(shí)調(diào)用了,請(qǐng)?zhí)顚?xiě)正確的年齡");}if(StringUtils.isBlank(user.getName())){throw new RuntimeException("被真實(shí)調(diào)用了,對(duì)不起,姓名不能為空");}return save(user);}@Transactional(rollbackFor = Exception.class)@Overridepublic Integer updateUser(User user) {System.out.println("執(zhí)行了真實(shí)的更新用戶(hù)方法");int result= getBaseMapper().updateById(user);System.out.println("update user result:"+result);return result;}}
編寫(xiě)controller
package com.lkcoffee.framework.demo2.controller;import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;
import java.util.Objects;
import java.util.Optional;/*** @Desciption:* @Author: feixiang.li* @date: 2024-07-12 10:45**/
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@GetMappingpublic List<User> queryAll(){return userService.queryAll();}@GetMapping("/{id}")public User queryById(@PathVariable Long id){if(Objects.isNull(id)){return new User();}return userService.queryById(id);}@PostMappingpublic String save(@RequestBody User user){if(Objects.isNull(user)){return "對(duì)象為空";}userService.save(user);return "success";}
}
啟動(dòng)項(xiàng)目: 訪問(wèn)下面
http://localhost:8080/user
返回一下結(jié)果,說(shuō)明項(xiàng)目啟動(dòng)成功;
3.2 測(cè)試
1) 創(chuàng)建Mock或者Spy對(duì)象
junit4 | junit5 | |
---|---|---|
方法一 | @RunWith(MockitojUnitRunner.class)+@Mock等注解 | @ExtendWith(MockitoExtension.class)+@Mock等注解 |
方法二 | Mockito.mock(X.class)MockitoAnnotations.open等靜態(tài)方法 | Mockito.mock(X.class)MockitoAnnotations.open等靜態(tài)方法 |
方法三 | Mocks(this)+@Mock等注解 | Mocks(this)+@Mock等注解 |
方法一
package com.lkcoffee.framework.demo2;/*** @Desciption:* @Author: feixiang.li* @date: 2024-07-12 14:38**/import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;@ExtendWith(MockitoExtension .class)
public class Test1 {@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@Testpublic void test1(){// 判斷某個(gè)對(duì)象是不是mock對(duì)象System.out.println("Mockito.mockingDetails(mockUserService).isMock(): "+ Mockito.mockingDetails(mockUserService).isMock());System.out.println("Mockito.mockingDetails(spyUserService).isSpy(): "+ Mockito.mockingDetails(spyUserService).isSpy());}
}
方法二
package com.lkcoffee.framework.demo2;/*** @Desciption:* @Author: feixiang.li* @date: 2024-07-12 14:38**/import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;public class Test2 {@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@BeforeEachpublic void init() {mockUserService=Mockito.mock(UserService.class);spyUserService=Mockito.spy(UserService.class);}@Testpublic void test1(){// 判斷某個(gè)對(duì)象是不是mock對(duì)象System.out.println("Mockito.mockingDetails(mockUserService).isMock(): "+ Mockito.mockingDetails(mockUserService).isMock());System.out.println("Mockito.mockingDetails(spyUserService).isSpy(): "+ Mockito.mockingDetails(spyUserService).isSpy());}
}
方法三
package com.lkcoffee.framework.demo2;/*** @Desciption:* @Author: feixiang.li* @date: 2024-07-12 14:38**/import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;public class Test3 {@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@BeforeEachpublic void init() {MockitoAnnotations.openMocks(this);}@Testpublic void test1(){// 判斷某個(gè)對(duì)象是不是mock對(duì)象System.out.println("Mockito.mockingDetails(mockUserService).isMock(): "+ Mockito.mockingDetails(mockUserService).isMock());System.out.println("Mockito.mockingDetails(spyUserService).isSpy(): "+ Mockito.mockingDetails(spyUserService).isSpy());}
}
MockitoAnnotations.initMocks(this)和MockitoAnnotations.openMocks(this)
這兩個(gè)效果一樣,只是在juit5中initMocks被拋棄了
MockitoAnnotations.initMocks(this)方法并不會(huì)產(chǎn)生代理類(lèi),它主要是用于初始化Mockito注解。在測(cè)試中,我們通常使用@Mock、@Spy、@InjectMocks等注解來(lái)創(chuàng)建Mock對(duì)象,并使用Mockito.when、Mockito.verify等方法來(lái)模擬對(duì)象的行為和驗(yàn)證方法調(diào)用。
但是,如果我們不調(diào)用MockitoAnnotations.initMocks(this)方法,這些Mock對(duì)象就無(wú)法被正確初始化,從而導(dǎo)致測(cè)試失敗。因此,我們通常在@Before注解方法中調(diào)用這個(gè)方法,以確保所有的Mock對(duì)象都已經(jīng)被正確初始化。
在具體實(shí)現(xiàn)中,MockitoAnnotations.initMocks(this)方法會(huì)掃描測(cè)試類(lèi)中所有的@Mock、@Spy、@InjectMocks注解,并根據(jù)注解中的類(lèi)型和名稱(chēng)來(lái)創(chuàng)建對(duì)應(yīng)的Mock對(duì)象,并將這些對(duì)象注入到測(cè)試類(lèi)中。這樣,在測(cè)試過(guò)程中就可以使用這些Mock對(duì)象來(lái)模擬外部依賴(lài),從而實(shí)現(xiàn)單元測(cè)試的獨(dú)立性和可重復(fù)性。
2) 參數(shù)匹配
package com.lkcoffee.framework.demo2;/*** @Desciption:* @Author: feixiang.li* @date: 2024-07-12 14:38**/import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;/*** 參數(shù)匹配:通過(guò)方法簽名(參數(shù))來(lái)制定哪些方法調(diào)用需要被處理*/
@ExtendWith(MockitoExtension.class)
public class ParamMatcherTest {private final Logger log = LoggerFactory.getLogger(ParamMatcherTest.class);@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@Testpublic void test2() {/*** 這里返回值是null. Mock對(duì)象不會(huì)調(diào)用真實(shí)方法*/User user = new User();user.setId(1L);user.setName("fly");doReturn(99).when(mockUserService).updateUser(user);int result1 = mockUserService.updateUser(user);log.info("用戶(hù)1修改對(duì)象返回值:{}", result1);User user2 = new User();user.setId(2L);user.setName("name2");int result2 = mockUserService.updateUser(user2);log.info("用戶(hù)2修改對(duì)象返回值:{}", result2);// 現(xiàn)在我想任意用戶(hù)都返回99doReturn(99).when(mockUserService).updateUser(any());result1 = mockUserService.updateUser(user);result2 = mockUserService.updateUser(user2);log.info("用戶(hù)1修改對(duì)象返回值:{}", result1);log.info("用戶(hù)2修改對(duì)象返回值:{}", result2);}@Testpublic void test1() {/*** 這里返回值是null. Mock對(duì)象不會(huì)調(diào)用真實(shí)方法。如果不進(jìn)行插樁的話(huà)*/User user = mockUserService.queryById(1L);log.info("user:{}", user);}
}
3) 打樁
package com.lkcoffee.framework.demo2;import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;@ExtendWith(MockitoExtension.class)
public class App4Test {private final Logger log= LoggerFactory.getLogger(App4Test.class);public static class MockitoTestController{public int add(int a, int b){System.out.println("調(diào)用了真實(shí)方法 測(cè)試了a+b a="+a+",b="+b);return a+b;}}@Spyprivate MockitoTestController spyMockitoTestController;@BeforeEachvoid setUp() {}/*** 測(cè)試add方法* 該方法模擬調(diào)用mockitoTestController的add方法,傳入?yún)?shù)1和2,期望返回值為3。* 首先,通過(guò)when語(yǔ)句設(shè)置mockitoTestController的add方法返回值為3;* 然后,使用assertThat斷言驗(yàn)證調(diào)用add方法(1, 2)實(shí)際返回值確實(shí)為3;* 最后,通過(guò)verify語(yǔ)句確認(rèn)mockitoTestController的add方法確實(shí)被調(diào)用了一次,并傳入了參數(shù)1和2。*/@Testvoid testAdd() {// 設(shè)置mock對(duì)象的行為(打樁),當(dāng)調(diào)用add(1, 2)時(shí)返回4// 雖然使用了when ,但是已經(jīng)調(diào)用了真實(shí)方法when(spyMockitoTestController.add(1, 2)).thenReturn(4);// 調(diào)用mock對(duì)象的方法,返回為4int result = spyMockitoTestController.add(1, 2);log.info("mockitoTestController.add result={}",result);// 斷言驗(yàn)證:調(diào)用add(1, 2)方法返回值是否為4Assertions.assertEquals(spyMockitoTestController.add(1, 2),4);// 驗(yàn)證:確保add方法(1, 2)被調(diào)用了一次verify(spyMockitoTestController,times(2)).add(1, 2);/*** spy對(duì)象在沒(méi)有攝性時(shí)是謂用真實(shí)方法的,號(hào)加en中會(huì)導(dǎo)致先技行一次方法,達(dá)不打樁的目的* 需使用 doXxx().when(obj).someNethod()*/doReturn(99).when(spyMockitoTestController).add(anyInt(),anyInt());int result2 = spyMockitoTestController.add(1, 2);log.info("spyMockitoTestController.add result={}",result2);}}
如果使用springboot的話(huà),低端用法,沒(méi)有使用@SpringbootTest
和@SpyBean
注解
package com.lkcoffee.framework.demo2;/*** @Desciption:* @Author: feixiang.li* @date: 2024-07-12 14:38**/import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.mapper.UserMapper;
import com.lkcoffee.framework.demo2.service.UserService;
import com.lkcoffee.framework.demo2.service.impl.UserServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;@ExtendWith(MockitoExtension.class)
public class Test5 {@Mockprivate UserMapper userMapper;@Mockprivate UserServiceImpl mockUserService;@InjectMocks@Spyprivate UserServiceImpl spyUserService;@Testpublic void test1() {// 這一步是為了解決mybatisplus的問(wèn)題。手動(dòng)把mapper注入進(jìn)去。// 如果使用了Autowired 的 Resource ,就不需要這一步了doReturn(userMapper).when(spyUserService).getBaseMapper();User user = new User();user.setId(1L);user.setName("name1");when(userMapper.updateById(any(User.class))).thenReturn(-1);when(mockUserService.updateUser(user)).thenReturn(99);int result1 = mockUserService.updateUser(user);System.out.println("result1 = " + result1);when(spyUserService.updateUser(user)).thenReturn(99);int result2 = spyUserService.updateUser(user);System.out.println("result2 = " + result2);/*** spy對(duì)象在沒(méi)有攝性時(shí)是謂用真實(shí)方法的,號(hào)加en中會(huì)導(dǎo)致先技行一次方法,達(dá)不och的目的* 需使用 doXxx().when(obj).someNethod()*/doReturn(100).when(spyUserService).updateUser(any());int result3 = spyUserService.updateUser(user);System.out.println("result3 = " + result3);}
}
執(zhí)行結(jié)果對(duì)象
result1 = 99
執(zhí)行了真實(shí)的更新用戶(hù)方法
update user result:-1
result2 = 99
result3 = 100
4) 多次打樁
package com.lkcoffee.framework.demo2;/*** @Desciption:* @Author: feixiang.li* @date: 2024-07-12 14:38**/import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.mapper.UserMapper;
import com.lkcoffee.framework.demo2.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;import java.util.List;import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;@ExtendWith(MockitoExtension.class)
public class Test6 {@Mockprivate List<Integer> mockList;@Testpublic void test1() {//第1次調(diào)用返回1,第2次調(diào)用返回2,第3次及之后的調(diào)用都返回3// when(mockList.size()).thenReturn(1).thenReturn(2).thenReturn(3),// 可簡(jiǎn)寫(xiě)為:when(mockList.size()).thenReturn(1, 2, 3);Assertions.assertEquals(1, mockList.size());Assertions.assertEquals(2, mockList.size());Assertions.assertEquals(3, mockList.size());Assertions.assertEquals(3, mockList.size());}
}
5) 實(shí)戰(zhàn)
package com.lkcoffee.framework.demo2;import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.mapper.UserMapper;
import com.lkcoffee.framework.demo2.service.UserService;
import com.lkcoffee.framework.demo2.service.impl.UserServiceImpl;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;import java.util.List;import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;@SpringBootTest(classes = Demo2Application.class)
class UserServiceImplTest {@MockBeanprivate UserMapper userMapper;@Resource@SpyBeanprivate UserServiceImpl userService;@BeforeEachvoid setUp() {// 這一步是為了解決mybatisplus 中沒(méi)有baseMapper的問(wèn)題// 因?yàn)槭抢^承了ServiceImpl 。是父類(lèi)。InjectMocks無(wú)法注入父類(lèi)的屬性// 如果使用了Autowired 的 Resource ,就不需要這一步了// doReturn(userMapper).when(userService).getBaseMapper();}@Testvoid testQueryAll() {// 模擬查詢(xún)結(jié)果when(userMapper.selectList(any())).thenReturn(List.of(new User(1L, "Alice", 25,"203462009@qq.com"),new User(2L, "Bob", 30,"203462008@qq.com")));// 執(zhí)行查詢(xún)var result = userService.queryAll();// 驗(yàn)證查詢(xún)結(jié)果assertEquals(2, result.size());assertEquals("Alice", result.get(0).getName());assertEquals("Bob", result.get(1).getName());}@Testvoid testQueryById() {// 模擬查詢(xún)結(jié)果when(userMapper.selectById(1L)).thenReturn(new User(1L, "Alice", 25,"203462009@qq.com"));// 執(zhí)行查詢(xún)var result = userService.queryById(1L);// 驗(yàn)證查詢(xún)結(jié)果assertEquals("Alice", result.getName());}@Testvoid testAddUser() {// 創(chuàng)建一個(gè)用戶(hù)對(duì)象User user = new User(null, "Alice", 25,"203462009@qq.com");// 模擬save方法返回結(jié)果when(userMapper.insert(user)).thenReturn(1);// 執(zhí)行添加用戶(hù)var result = userService.addUser(user);// 驗(yàn)證添加結(jié)果assertTrue(result);}
}
3.3 Springboot測(cè)試注解
@MockBean
@MckBean
是Spring Boot提供的注解,專(zhuān)門(mén)用于在Spring應(yīng)用上下文中添加或替換一個(gè)bean為mock對(duì)象。這個(gè)注解主要用于集成測(cè)試場(chǎng)景,特別是當(dāng)測(cè)試需要Spring環(huán)境支持時(shí),如測(cè)試控制器與服務(wù)層的交互等。
- 使用@MockBean注解的的對(duì)象,會(huì)生成一個(gè)Mock的bean.不會(huì)生成原來(lái)的bean
- 并會(huì)將該bean注入到依賴(lài)該bean的其他bean中
- 正常的bean還是會(huì)正常組裝注入
Spring Boot 中@Mock 和@MockBean 注解的主要區(qū)別
@Mock
用于模擬不屬于 Spring 上下文的對(duì)象,而@MockBean
用于模擬屬于一部分的對(duì)象Spring上下文的。它用于帶有 Mockito 框架的普通 JUnit 測(cè)試。它也不知道 Spring 上下文,通常用于單元測(cè)試隔離組件,而不需要完整的 Spring 上下文設(shè)置。@MockBean
是一個(gè) Spring Boot 特定的注釋,它提供與 Spring Boot 測(cè)試模塊的集成,允許在 Spring Boot 應(yīng)用程序中無(wú)縫模擬 Spring bean。@Mock
需要使用 MockitoJUnitRunner 或 MockitoExtension 來(lái)初始化模擬對(duì)象,而@MockBean在測(cè)試上下文設(shè)置期間由 Spring Boot 測(cè)試框架自動(dòng)初始化。@MockBean
在測(cè)試時(shí)將Spring上下文中的實(shí)際bean替換為mock對(duì)象,而@Mock
不影響Spring上下文中的實(shí)際bean
@SpyBean
@SpringBootTest(classes = AppBootStrap.class)
public class AbstractTestCase {}/*** 1。使用@MockBean注解的的對(duì)象,會(huì)生成一個(gè)Mock的bean.不會(huì)生成原來(lái)的bean* 2。并會(huì)將該bean注入到依賴(lài)該bean的其他bean中* 3。正常的bean還是會(huì)正常組裝注入*/
public class HelloControllerMockBeanTest extends AbstractTestCase {@Autowiredprivate HelloController helloController;@MockBeanprivate HelloService helloService;@Testpublic void testHello(){System.out.println("============only junit5================");helloController.hello();System.out.println("============only junit5================");}
}/*** 1。使用@MockBean注解的的對(duì)象,會(huì)生成一個(gè)spy的bean行為與原類(lèi)型一致.不會(huì)生成原來(lái)的bean* 2。并會(huì)將該bean注入到依賴(lài)該bean的其他bean中* 3。正常的bean還是會(huì)正常組裝注入*/
public class HelloControllerSpyBeanTest extends AbstractTestCase {@Autowiredprivate HelloController helloController;@SpyBeanprivate HelloService helloService;@Testpublic void testHello(){System.out.println("============only junit5================");helloController.hello();System.out.println("============only junit5================");}
}