單元測試之mockito

簡介

mockito是一款模擬測試框架,用于Java開發中的單元測試。通過mockito,可以創建和配置一個對象,通過它來替換對象的外部依賴。

作用:模擬一個類的外部依賴,保證單元測試的獨立性。例如,在類A中會調用類B提供的功能,那么類A就依賴于類B,這個時候,為類A編寫的單元測試,依賴于類B提供的功能,但是類B可能是不穩定的,它可能是一個rpc接口、或者是一個dao接口,rpc接口可能會出現網絡問題,數據庫中的數據可能會被別人修改,所以,就使用mockito來模擬類B,將模擬出的實例注入到類A的實例中,此時,在為類A編寫的單元測試中,它依賴的模擬出的類B,它不再受具體外部環境的干擾,無論執行多少次都可以獲得相同的結果。通過mockito,保證了單元測試的獨立性,這是回歸測試的基礎,同時也是測試驅動開發的基本技術。

回歸測試:指修改了舊代碼后,重新執行以前的測試,以確認修改沒有引入新的錯誤或導致其它代碼產生錯誤

測試驅動開發:Test-Driven Development,TDD,在開發功能代碼之前,先編寫單元測試,測試代碼明確需要編寫什么產品代碼,是敏捷開發中的一項核心實踐和技術,也是一種設計方法論。

一個優秀的單元測試應該具備的特點:

  • 一個測試不應該依賴于外部環境
  • 一個單元測試不依賴與另一個單元測試的結果,單元測試之間的執行順序不會改變單元測試的結果
  • 單元測試的結尾必須是斷言

入門案例

在這個入門案例中,模擬mockito在實際開發中的使用場景,演示mockito在單元測試中究竟起到了什么作用,這也是我學習mockito之前最困惑的一點。

環境準備

第一步:編輯pom文件,添加junit、mockito、servlet、lombok依賴

<!--junit-->
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope>
</dependency><!--mockito -->
<dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>2.23.4</version><scope>test</scope>
</dependency><!--servlet依賴-->
<dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.0.1</version><scope>provided</scope>
</dependency><!--lombok-->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version><scope>compile</scope>
</dependency>

第二步:編寫實體類Account

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {private String username;private String password;
}

第三步:編寫mapper類

public class AccountMapper {public Account findAccount(String username, String password) {return new Account("aa", "12345");}public Boolean existsAccount(Account account) {return false;}
}

第四步:編寫控制器

/*** 模擬一個常見的控制器*/
public class AccountController {private final AccountMapper mapper;public AccountController(AccountMapper mapper) { this.mapper = mapper; }public String login(HttpServletRequest request) {final String username = request.getParameter("username");final String password = request.getParameter("password");try {Account account = mapper.findAccount(username, password);if(account == null) {return "/login";} else {return "/index";}} catch (Exception e) {Utils.println("登錄出現異常");e.printStackTrace();return "/505";}}
}

需求

需求:編寫AccountController的單元測試

上述代碼是在模擬一個實際的項目,當然實際開發會比這更加復雜,但是用于了解mockito的功能是比較合適的。

在上述代碼中,AccountController依賴于AccountMapper,假設AccountMapper是一個rpc接口,需要在指定的環境中調用,但是一個好的單元測試不應該依賴于外部環境,它最好可以重復執行,此時,需要使用mockito來模擬AccountMapper,使開發者可以專注于測試AccountController中的功能。

完成需求

編寫測試案例,使用mockito,模擬外部依賴

案例1:使用mock方法來創建AccountMapper的模擬對象,同時在模擬對象上進行方法打樁,設置模擬對象的行為

@Test
public void test2(){// 創建AccountMapper的模擬對象AccountMapper mapper = Mockito.mock(AccountMapper.class);// 方法打樁:設置模擬對象的行為Mockito.when(mapper.findAccount("aa", "123")).thenReturn(null);// 執行被測試類中的方法AccountController controller = new AccountController(mapper);String loginResult = controller.login("aa", "123");// 斷言:設置mapper.findAccount()方法的返回值為null,代表登錄失敗,返回登錄頁面Assert.assertEquals("/login", loginResult);
}

案例2:同案例1一樣,只不過這次設置模擬對象的方法拋出異常

@Test 
public void test3(){AccountMapper mapper = Mockito.mock(AccountMapper.class);// 方法打樁:拋出異常Mockito.when(mapper.findAccount("aa", "123")).thenThrow(new RuntimeException());AccountController controller = new AccountController(mapper);String loginResult = controller.login("aa", "123");// 斷言:被測試方法返回'/505'Assert.assertEquals("/505", loginResult);
}

入門案例講解

模擬對象:使用mock方法創建AccountMapper的模擬對象,將它注入到AccountController中,此時,AccountController的依賴被替換為模擬對象,它不再依賴于具體的環境,也就是真實的AccountMapper實例。

方法打樁:使用when方法、thenReturn方法、thenThrows方法,來設計模擬對象的行為。

概念和特性

模擬對象:mockito可以創建模擬對象,代替真實的對象作為被測試類的依賴,這樣可以在測試中完全控制這些對象的行為和返回值。

方法打樁:通過方法打樁設置預期行為,用戶可以定義模擬對象在接收到特定方法調用時應如何響應,比如返回特定值或拋出異常。

監視:mockito可以監視真實的對象或模擬對象上的方法調用,用于隨后驗證。

驗證:在測試結束后檢查模擬對象是否如預期那樣被調用了正確的方法和次數。

基本使用

在之前的案例中,學習了mockito的使用場景,和基本的使用方法。我一開始接觸mockito的時候,最困惑的就是它的使用場景,我不明白為什么要把單元測試搞得這么復雜,學完之后才解開了自己的困惑,所以在這里我把使用場景放在最開頭,接下來詳細地了解mockito中的各項功能。

創建模擬對象

調用mock方法,創建模擬對象

@Test
public void test1() {// 創建一個mock對象List mockList = Mockito.mock(List.class);// 判斷mock對象的類型assert mockList instanceof List;
}

方法打樁:設置方法正常返回

配置調用模擬對象的某個方法時的返回值

@Test
public void test2() {List mockList = Mockito.mock(List.class);// 方法打樁:配置模擬對象上某個方法的行為,這里配置add("one")時返回trueMockito.when(mockList.add("one")).thenReturn(true);assert mockList.add("one");  // trueassert !mockList.add("two"); // false// 方法打樁,配置模擬對象調用size()方法時返回1Mockito.when(mockList.size()).thenReturn(1);assert mockList.size() == 1;
}

方法打樁:設置方法拋異常

配置模擬對象拋出異常

@Test
public void test3() {List mockList = Mockito.mock(List.class);Mockito.when(mockList.remove("aa")).thenThrow(new NoSuchElementException("沒有該元素"));String msg = null;try {mockList.remove("aa");} catch (NoSuchElementException e) {msg = e.getMessage();}assert msg.equals("沒有該元素");
}

為返回值為void的方法進行打樁

這里需要使用doThrow方法

@Test(expected = RuntimeException.class)
public void test7() {List mockList = Mockito.mock(List.class);Mockito.doThrow(new RuntimeException("異常")).when(mockList).add(1, 1);mockList.add(1, 1);
}

檢測模擬對象的方法調用

mockito會追蹤模擬對象的所有方法調用和調用方法時傳遞的參數,使用verify方法,可以檢測指定方法的調用是否符合要求

@Test
public void test4() {List mockList = Mockito.mock(List.class);mockList.add(1);mockList.add(2);mockList.add(2);Mockito.verify(mockList, Mockito.times(1)).add(1);Mockito.verify(mockList, Mockito.times(2)).add(2);Mockito.verify(mockList, Mockito.atLeastOnce()).add(1);Mockito.verify(mockList, Mockito.times(0)).isEmpty();
}

監視真實對象

調用spy方法,可以包裝一個真實的對象,如果spy對象沒有設置打樁,所有的方法都會調用對象實際的方法,使用這種方式,可以對于存量代碼進行單測。

有些時候不想對一個對象進行mock,但是想判斷一個普通對象的方法有沒有被調用過,那么可以使用spy方法來監測對象,然后用verify 來驗證方法有沒有被調用。

@Test
public void test5() {List<String> list = new ArrayList<>();List<String> spyList = Mockito.spy(list);spyList.add("1");spyList.add("2");spyList.add("3");// 方法打樁Mockito.when(spyList.size()).thenReturn(1);assert spyList.size() == 1; // 調用打樁后的方法而不是真實的方法
}

參數匹配器

更加靈活地進行打樁和驗證,例如anyInt(),代表任意大小的int值

@Test
public void test6() {List mockList = Mockito.mock(List.class);Mockito.when(mockList.get(Mockito.anyInt())).thenReturn("aaa");assert mockList.get(0).equals("aaa");assert mockList.get(8).equals("aaa");
}

設置調用一個方法時的具體行為

thenAnswer方法,它可以設置調用一個方法時的具體行為,而不是像thenReturn一樣,返回一個具體值

@Test
public void test(){AccountMapper mapper = Mockito.mock(AccountMapper.class);// 設置調用一個方法時的具體行為,在這里比較簡單,知識返回一個具體的對象Mockito.when(mapper.findAccount("aa", "123")).thenAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocation) throws Throwable {return new Account("bb", "234");}});AccountController con = new AccountController(mapper);String loginResult = con.login("aa", "123");// 登錄成功Assert.assertEquals("/index", loginResult);
}

驗證方法的調用次數

public void test9() {List mockList = Mockito.mock(List.class);Mockito.when(mockList.get(0)).thenReturn("123");assert mockList.get(0).equals("123");Mockito.verify(mockList, Mockito.times(1)).get(0); // 驗證指定方法被調用了一次
}

模擬靜態方法

在Mockito的早期版本中,它不支持直接模擬靜態方法。但是,從Mockito 3.4.0版本開始,Mockito通過擴展庫mockito-inline提供了對模擬靜態方法的支持。

模擬靜態方法應該謹慎使用,因為靜態方法通常作為全局狀態或工具方法,它們的模擬可能會影響程序的其他部分。

添加依賴:

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

編寫代碼:

@Test
public void mulTest() {try (MockedStatic<CalculateUtils> theMock = Mockito.mockStatic(CalculateUtils.class)) {//對CalculateUtil.mul(11,22)進行mock,讓其返回99Mockito.when(CalculateUtils.mul(11, 22)).thenReturn(99);//調用int result = CalculateUtils.mul(11, 22);assert result == 99;}
}

對于mockito 來說,一旦聲明了一個 MockedStatic,它會一直留在當前的線程中并且會對當前線程中所有調用的代碼產生影響,這個影響不僅僅局限于單元測試,甚至會對測試框架(TestNG,Junit等)產生影響,所以一定要保證在測試代碼結束后對 MockedStatic 進行關閉,否則可能會對其他單元測試產生影響。在jdk1.8中,可以通過try resource語句來關閉MockedStatic

注解

常用注解:

  • @Mock:相當于mock方法
  • @Spy:相當于spy方法
  • @InjectMocks:把被@Mock和@Spy修飾的成員變量注入到當前成員變量中

使用案例:

public class Mockito3Test {@InjectMocks@Spy //加上@Spy,表示監視真實的對象,同時防止mock多線程運行報錯private AccountController controller;@Mockprivate AccountMapper mapper;@Beforepublic void before() {MockitoAnnotations.initMocks(this);  // 使用注解前,創建環境,使注釋生效}@Testpublic void test() {String login = controller.login("aa", "123");// 模擬登錄失敗Mockito.when(mapper.findAccount("aa", "123")).thenReturn(null);Assert.assertEquals("/login", login);  // /返回login表示登錄失敗}
}

總結

在使用mockito這一章節中,總結了mockito的常用功能,同時結合在入門案例中提到了mockito的使用場景,來學習mockito的常見用法。

使用mock方法,來創建一個模擬對象,將模擬對象注入到待測試的實例中,此時,待測試的實例依賴的是模擬對象而不是真實對象,通過模擬對象,可以獲得穩定的外部依賴,保證單元測試可以重復執行。

使用spy方法,監視一個真實的對象,隨后可以調用verify方法來驗證對于真個真實對象的調用情況。

mock方法和spy方法的區別在于,mock方法接收一個類對象作為參數,根據這個類對象創建一個模擬對象,spy方法接收一個實例作為參數,它會監視這個實例。

常用API總結

在之前的章節中學習了mockito的使用場景和具體功能,其中涉及到了很多api,在這里記錄一下這些api的基本功能

org.mockito

Mockito:public class Mockito extends ArgumentMatchers:提供了mockito的核心功能

  • mock方法:public static <T> T mock(Class<T> classToMock):使用參數指定的類對象創建一個mock對象。具體方式是,在內存中動態地創建一個類,這個類是參數指定的類的子類,然后創建這個類的實例,這個實例就是mock對象,由它來完成方法打樁、調用統計等功能。

  • when方法:public static <T> OngoingStubbing<T> when(T methodCall):方法打樁,打樁是指設置方法在指定情況下的行為,例如,傳入一個參數,返回一個結果,這種設置并不會改變方法本身的行為,它的作用是在模擬的對象上設置方法的行為,方便測試,類似于造數據。

  • spy方法: public static <T> T spy(Class<T> classToSpy):使用spy方法模擬出的對象,會實際調用類中的方法,除非用戶設置了方法打樁。參數可以傳入一個類對象或一個實際的對象

  • verify方法:public static <T> T verify(T mock):驗證某些之前發生過一次的行為,如果這些行為發生過,沒有問題,如果沒有發生過,報錯。驗證方法行為的案例:

List<Object> list = mock(List.class);
list.add("aa");
list.add("bb");
verify(list).add("aa");   // 不報錯
verify(list).add("cc");   // 報錯
  • reset:public static <T> void reset(T... mocks):重置,之前在這個對象上的打樁方法全部消除
  • anyInt:public static int anyInt():返回任意int類型的數據
  • argThat:public static <T> T argThat(ArgumentMatcher<T> matcher):參數匹配器

OngoingStubbing:public interface OngoingStubbing<T>:方法打樁時返回的接口

  • thenReturn:OngoingStubbing<T> thenReturn(T value):設置返回值,用作實參的方法調用必須有一個返回值
  • thenThrow:OngoingStubbing<T> thenThrow(Throwable... throwables):設置拋出的異常
  • thenAnswer:OngoingStubbing<T> thenAnswer(Answer<?> answer):設置返回值,可以根據參數進行計算
  • thenCallRealMethod:OngoingStubbing<T> thenCallRealMethod():設置,當被模擬出的對象上的方法被調用時,調用真實的方法

源碼分析

mockito中的幾個基本功能:

  • 通過mock方法創建一個類的模擬實例
  • 通過spy方法監視一個真實的對象
  • when和thenReturn方法配合實現方法打樁。
  • verify方法驗證模擬對象的行為

mockito的基本原理,是生成被mock類的子類,用戶持有這個子類的實例,通過這個子類,實現方法打樁的功能,所以mockito不支持模擬靜態方法、私有方法、被final修飾的方法,因為它們無法被繼承。接下來研究mockito究竟是怎么做到的。

mock方法

案例:

@Test
public void test1() throws InterruptedException {// 創建一個mock對象List mockList = Mockito.mock(List.class);// 打印mock對象的類名:org.mockito.codegen.List$MockitoMock$960824855System.out.println("mockList.getClass().getName() = " + mockList.getClass().getName());// 判斷mock對象的類型assert mockList instanceof List;
}

整體流程:進入mock方法,經過一系列調用,進入MockitoCore的mock方法,這個方法中定義了創建mock實例的整體流程

// 參數1是要mock的類的類對象,在這里是List.class,參數2是默認配置
public <T> T mock(Class<T> typeToMock, MockSettings settings) {// 配置類實例是默認創建的,在這里判斷如果它不是MockSettingsImpl類型,拋異常if (!MockSettingsImpl.class.isInstance(settings)) {throw new IllegalArgumentException("Unexpected implementation of '" +settings.getClass().getCanonicalName() + "'\nAt the moment, you cannot provide your own implementations of that class.");} else {// 獲取配置類實例MockSettingsImpl impl = (MockSettingsImpl)MockSettingsImpl.class.cast(settings);// 構造創建mock實例時的配置信息MockCreationSettings<T> creationSettings = impl.build(typeToMock);// 創建mock實例T mock = MockUtil.createMock(creationSettings);// 將mock實例存放到ThreadLocal中ThreadSafeMockingProgress.mockingProgress().mockingStarted(mock, creationSettings);return mock;}
}

第一步:構造創建mock實例時的配置信息

// build方法最終調用validateSettings方法,根據類對象判斷該類是否可以被mock
private static <T> CreationSettings<T> validatedSettings(Class<T> typeToMock, CreationSettings<T> source) {// 創建校驗器MockCreationValidator validator = new MockCreationValidator();// 校驗類對象的類型,底層是一個native方法,校驗類對象是否可變,同時類對象不是String.class或包裝類的類對象validator.validateType(typeToMock);validator.validateExtraInterfaces(typeToMock, source.getExtraInterfaces());validator.validateMockedType(typeToMock, source.getSpiedInstance());validator.validateConstructorUse(source.isUsingConstructor(), source.getSerializableMode());// 構造存儲配置信息的實例CreationSettings<T> settings = new CreationSettings(source);settings.setMockName(new MockNameImpl(source.getName(), typeToMock, false));settings.setTypeToMock(typeToMock);settings.setExtraInterfaces(prepareExtraInterfaces(source));return settings;
}

第三步:探究第一步中 “創建mock實例” 時做了什么,T mock = MockUtil.createMock(creationSettings);

public static <T> T createMock(MockCreationSettings<T> settings) {// 創建mockHandlerMockHandler mockHandler = MockHandlerFactory.createMockHandler(settings);// 創建mock實例T mock = mockMaker.createMock(settings, mockHandler);Object spiedInstance = settings.getSpiedInstance();if (spiedInstance != null) {(new LenientCopyTool()).copyToMock(spiedInstance, mock);}return mock;
}
// 上一步中的createMock方法,經過一系列的調用,最終調用MockMaker中的crateMock方法
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {// 這里是使用字節碼生成技術,創建一個類對象Class<? extends T> type = this.createMockType(settings);Instantiator instantiator = Plugins.getInstantiatorProvider().getInstantiator(settings);try {// 創建mock類的實例T instance = instantiator.newInstance(type);// 創建方法攔截器,用戶通過mock實例調用方法時,在內部會先調用攔截器 MockMethodInterceptorMockMethodInterceptor mockMethodInterceptor = new MockMethodInterceptor(handler, settings);this.mocks.put(instance, mockMethodInterceptor);if (instance instanceof MockAccess) {((MockAccess)instance).setMockitoInterceptor(mockMethodInterceptor);}return instance;} catch (InstantiationException var7) {InstantiationException e = var7;throw new MockitoException("Unable to create mock instance of type '" + type.getSimpleName() + "'", e);}
}

mock方法創建出的實例:使用arthas來查看mockito創建出的字節碼,具體方法是先打印出類的全限定名,然后在arthas中直接查看這個類的字節碼信息

package org.mockito.codegen;import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import org.mockito.codegen.List$MockitoMock$1223363968$auxiliary$CVDZlt15;
import org.mockito.codegen.List$MockitoMock$1223363968$auxiliary$H1mHXgMT;
import org.mockito.internal.creation.bytebuddy.MockAccess;
import org.mockito.internal.creation.bytebuddy.MockMethodAdvice;
import org.mockito.internal.creation.bytebuddy.MockMethodInterceptor;public class List$MockitoMock$1223363968
implements List,
MockAccess {private static final long serialVersionUID = 42L;private MockMethodInterceptor mockitoInterceptor;private static final /* synthetic */ Method cachedValue$l5T7Iaqy$sgg2351;static {cachedValue$l5T7Iaqy$479u1c1 = List.class.getMethod("size", new Class[0]);cachedValue$l5T7Iaqy$2ff4l01 = List.class.getMethod("get", Integer.TYPE);cachedValue$l5T7Iaqy$sgg2351 = List.class.getMethod("add", Object.class);   }// 這里省略了一些方法// 從生成的實例中可以看到,用戶調用mock實例的方法時,在內部實際上是調用MockMethodOInterceptor中的方法,// 這里具體是調用MockMethodInterceptor的內部類DispatcherDefaultingToRealMethod中的// interceptAbstract方法@Overridepublic boolean add(Object object) {return (Boolean)MockMethodInterceptor.DispatcherDefaultingToRealMethod.interceptAbstract(this, this.mockitoInterceptor, false, cachedValue$l5T7Iaqy$sgg2351, new Object[]{object});}@Overridepublic void setMockitoInterceptor(MockMethodInterceptor mockMethodInterceptor) {this.mockitoInterceptor = mockMethodInterceptor;}
}

spy方法

spy方法底層也是調用mock方法,只不過傳入的配置信息不同,spy方法傳入的配置信息表示要調用mock對象的真實方法

@CheckReturnValue
public static <T> T spy(T object) {return MOCKITO_CORE.mock(object.getClass(),withSettings().spiedInstance(object).defaultAnswer(CALLS_REAL_METHODS));
}

when方法和thenReturn方法

測試案例:

@Test
public void test2() {List mockList = Mockito.mock(List.class);// 方法打樁:配置模擬對象上某個方法的行為,這里配置add("one")時返回trueMockito.when(mockList.add("one")).thenReturn(true);assert mockList.add("one");  // trueassert !mockList.add("two"); // false
}

方法打樁的整體流程:方法打樁時,首先執行mock對象上的方法,然后執行when方法,然后執行thenReturn方法。

  • mock對象上的方法:mock對象是mockito生成的,它的內部會調用攔截器,記錄當前方法的參數信息,生成invocation實例,存儲到ThreadLocal中,
  • 執行when方法:取出invocation實例,生成打樁對象OnGoingStub
  • 執行thenReturn方法:把參數添加到invocation實例中,從而完成方法打樁。
  • 最終,用戶通過mock對象調用指定方法時,mock對象會根據方法名和參數信息,查看ThreadLocal中有沒有存儲相應的打樁信息,如果有,返回打樁時設置的返回值。

總結:核心原理是攔截器加ThreadLocal,mock對象內部調用攔截器來生成調用信息,把它放在ThreadLocal中,后面都是通過ThreadLocal在線程內傳遞參數的。

實戰案例

springboot整合mockito

通過一個實際場景,來學習springboot整合mockito的作用。

假設有如下場景,現在有一個UserController,UserController有兩個依賴,UserService和LogService,UserService是一個rpc接口,LogService是一個日志記錄接口,不依賴外部環境,UserController中的方法都有注解,這些注解會被切面類處理,在切面類中實現權限校驗功能。

流程圖:

在這里插入圖片描述

代碼:

UserController

@RestController
@RequestMapping("/api/v1/user")
public class UserController {@Autowiredprivate IUserService userService;@Autowiredprivate ILogService logService;@PostMapping("/create")@AuthValidate(permission = PermissionEnum.CREATE_UPDATE_USER)public String create(@RequestBody String requestBody) {logService.log("接受到請求:" + requestBody);UserCreateVO userCreateVO = JsonUtil.fromJson(requestBody, UserCreateVO.class);UserDO userDO = convertUserCreateVO2DO(userCreateVO);int id;try {id = userService.create(userDO);} catch (Exception e) {logService.log("創建失敗:" + e.getMessage());return ResponseBody.fail("創建失敗:" + e.getMessage()).toJson();}return ResponseBody.success(id).toJson();}private UserDO convertUserCreateVO2DO(UserCreateVO userCreateVO) {UserDO userDO = new UserDO();BeanUtils.copyProperties(userCreateVO, userDO);Date date = new Date();userDO.setCreateUser("unknown");userDO.setCreateTime(date);userDO.setModifyUser("unknown");userDO.setModifyTime(date);return userDO;}
}

注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AuthValidate {PermissionEnum permission();
}

處理注解的切面類:

@Component
@Aspect
public class AuthValidateAspect {private static final Logger LOG = LoggerFactory.getLogger(AuthValidateAspect.class);@Pointcut("@annotation(org.wyj.beans.annotations.AuthValidate)")public void pointcut() { }@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return ResponseBody.fail("用戶 " + "unknown" + " 沒有權限").toJson();}HttpServletRequest request = attributes.getRequest();String userName = request.getHeader("userName");if ("zs".equals(userName)) {return joinPoint.proceed();} else {return ResponseBody.fail("用戶 " + userName + " 沒有權限").toJson();}}
}

現在的需求是,要為UserController編寫單元測試。

使用mockito,可以很輕松的為UserController編寫單元測試。代碼如下:

public class UserControllerTest {@InjectMocks@Spyprivate UserController userController;@Mockprivate ILogService logService;@Mockprivate IUserService userService;@BeforeEachpublic void beforeEach() {MockitoAnnotations.openMocks(this);}// 正例:創建用戶成功@Testpublic void test1() {// 模擬外部依賴UserDO userDO = new UserDO();userDO.setName("張三");userDO.setAge(18);Mockito.when(userService.create(Mockito.any())).thenReturn(1);Mockito.doNothing().when(logService).log("{log}");// 測試String s = userController.create(JsonUtil.toJson(userDO));// 斷言assert s != null;ResponseBody responseBody = JsonUtil.fromJson(s, ResponseBody.class);Integer data = ((Double) responseBody.getData()).intValue();assert data.equals(1);}
}

在上面的單測中,用戶可以直接運行單測,它是獨立的,不依賴外部環境,包括spring容器,但是它有一個不足,無法驗證注解是否生效,因為單測不是在spring容器中運行的。這就需要用到springboot整合mockito,單測在spring容器中運行,同時,使用mockito模擬外部依賴。要注意,其實這種方式在理論上已經脫離了單元測試的范疇,更加像是多個模塊之間的集成測試,但是把這種測試提前放到單元測試中完成,是比較推薦的,避免把問題遺留到聯調時。

添加依賴:

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.5</version><relativePath/>
</parent><artifactId>demo2</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--spring-boot提供的單測框架,框架中完成了對于mockito的整合--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.6</version></dependency><!--添加對于mock靜態方法的支持--><dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>3.4.0</version><exclusions><!--前面spring-boot-starter-test中已經有關于mockito-core的依賴了--><exclusion><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId></exclusion></exclusions><scope>test</scope></dependency>
</dependencies>

編寫單測:

@SpringBootTest(classes = App.class)
public class UserController2Test {@Autowiredprivate UserController userController;@MockBeanprivate IUserService userService;// 正例:權限校驗成功,用戶創建成功@Testpublic void test1() {try (MockedStatic<RequestContextHolder> theMock = Mockito.mockStatic(RequestContextHolder.class)) {// 模擬請求體HttpServletRequest request = Mockito.mock(HttpServletRequest.class);ServletRequestAttributes servletRequestAttributes = new ServletRequestAttributes(request);Mockito.when((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).thenReturn(servletRequestAttributes);Mockito.when(request.getHeader("userName")).thenReturn("zs");String requestBody = "{\n" +"    \"name\": \"張三\",\n" +"    \"age\": 18\n" +"}";Mockito.when(userService.create(Mockito.any())).thenReturn(1);// 調用目標方法String s = userController.create(requestBody);// 斷言ResponseBody responseBody = JsonUtil.fromJson(s, ResponseBody.class);int id = ((Double) responseBody.getData()).intValue();assert id == 1;}}
}

在上面的單測中,使用MockBean聲明要被mock并注入到UserController中的外部依賴,同時使用@SpringBootTest注解,指定單測運行在spring容器中,這樣就可以測試注解是否可以正確地應用到UserController上。

這就是springboot整合mockito的作用,它可以在spring容器中,mock指定模塊的外部依賴。

踩坑記錄

匹配器不可以和常量混合使用

mockito報錯 InvalidUseOfMatchersException,不正確地使用匹配器,例如any()、anyInt()等,匹配器不可以和常量混合使用

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/75857.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/75857.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/75857.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Oracle數據庫數據編程SQL<5 正則表達式函數*****>

Oracle 提供了一組強大的正則表達式函數,用于在 SQL 和 PL/SQL 中進行復雜的模式匹配和文本處理。這些函數基于 POSIX 標準正則表達式,功能強大且靈活。 目錄 一、Oracle 正則表達式函數概覽 二、函數詳解及示例 1. REGEXP_LIKE 2. REGEXP_INSTR 3. REGEXP_SUBSTR 4. …

el-tabs添加按鈕增加點擊禁止樣式

前置文章 一、vue使用element-ui自定義樣式思路分享【實操】 二、vue3&ts&el-tabs多個tab表單校驗 現狀確認 點擊添加按鈕&#xff0c;沒有點擊樣式&#xff0c;用戶感知不明顯沒有限制最大的tab添加數量&#xff0c;可以無限添加 調整目標&代碼編寫 調整目標…

DB-Mysql中TIMESTAMP與DATETIME的區別

文章目錄 ?存儲范圍??時區處理?存儲空間?默認值和自動更新??零值處理?適用場景?總結 在MySQL中&#xff0c;TIMESTAMP和DATETIME是兩種常用的日期時間數據類型&#xff0c;它們雖然都用于存儲日期和時間&#xff0c;但在多個方面存在顯著差異。以下是它們的主要區別&a…

Spring 中有哪些設計模式?

&#x1f9e0; 一、Spring 中常見的設計模式 設計模式類型Spring 中的應用場景單例模式創建型默認 Bean 是單例的工廠模式創建型BeanFactory、FactoryBean抽象工廠模式創建型ApplicationContext 提供多個工廠接口代理模式結構型AOP 動態代理&#xff08;JDK/CGLIB&#xff09;…

C# Winform 入門(3)之尺寸同比例縮放

放大前 放大后 1.定義當前窗體的寬度和高度 private float x;//定義當前窗體的寬度private float y;//定義當前窗臺的高度 2.接收當前窗體的尺寸大小 x this.Width;//存儲原始寬度ythis.Height;//存儲原始高度setTag(this);//為控件設置 Tag 屬性 3.聲明方法&#xff0c;獲…

從零開始的編程-java篇1.6.3

前言&#xff1a; 通過實踐而發現真理&#xff0c;又通過實踐而證實真理和發展真理。從感性認識而能動地發展到理性認識&#xff0c;又從理性認識而能動地指導革命實踐&#xff0c;改造主觀世界和客觀世界。實踐、認識、再實踐、再認識&#xff0c;這種形式&#xff0c;循環往…

【Redis】數據的淘汰策略

目錄 淘汰策略方案&#xff08;8種&#xff09; LRU和LFU策略的區別 使用建議 手搓LRU算法 方式一 方式二 大家好&#xff0c;我是jstart千語。今天和大家回來聊一下redis&#xff0c;這次要講的是它的淘汰策略。為什么需要淘汰策略呢&#xff0c;就是當redis里面的內存占…

【前端】Node.js一本通

近兩天更新完畢&#xff0c;建議關注收藏點贊。 目錄 復習Node.js概述使用fs文件系統模塊path路徑模塊 http模塊 復習 為什么JS可以在瀏覽器中執行 原理&#xff1a;待執行的JS代碼->JS解析引擎 不同的瀏覽器使用不同的 JavaScript 解析引擎&#xff1a;其中&#xff0c;C…

【AI論文】JavisDiT: 具備層次化時空先驗同步機制的聯合音視頻擴散Transformer

摘要&#xff1a;本文介紹了一種新型的聯合音頻-視頻擴散變換器JavisDiT&#xff0c;該變換器專為同步音頻-視頻生成&#xff08;JAVG&#xff09;而設計。 基于強大的擴散變換器&#xff08;DiT&#xff09;架構&#xff0c;JavisDiT能夠根據開放式用戶提示同時生成高質量的音…

Java-實現公有字段自動注入(創建人、創建時間、修改人、修改時間)

文章目錄 Mybatis-plus實現自動注入定義 MetaObjectHandler配置 MyBatis-Plus 使用 MetaObjectHandler實體類字段注解使用服務類進行操作測試 Jpa啟用審計功能實現自動注入添加依賴啟動類啟用審計功能實現AuditorAware接口實體類中使用審計注解 總結 自動注入創建人、創建時間、…

金融機構開源軟件風險管理體系建設

開源軟件為金融行業帶來了創新活力的同時&#xff0c;也引入了一系列獨特的風險。金融機構需要構建系統化的風險管理體系&#xff0c;以識別和應對開源軟件在全生命周期中的各種風險點。下面我們將解析開源軟件在金融場景下的主要風險類別&#xff0c;并探討如何建立健全的風險…

圖形渲染中的定點數和浮點數

三種API的NDC區別 NDC全稱&#xff0c;Normalized Device Coordinates Metal、Vulkan、OpenGL的區別如下&#xff1a; featureOpenGL NDCMetal NDCVulkan NDC坐標系右手左手右手z值范圍[-1,1][0,1][0,1]xy視口范圍[-1,1][-1,1][-1,1] GPU渲染的定點數和浮點數 定點數類型&a…

同花順客戶端公司財報抓取分析

目標客戶端下載地址:https://ft.51ifind.com/index.php?c=index&a=download PC版本 主要難點在登陸,獲取token中的 jgbsessid (每次重新登錄這個字段都會立即失效,且有效期應該是15天的) 抓取jgbsessid 主要通過安裝mitmproxy 使用 mitmdump + 下邊的腳本實現監聽接口…

QT工程建立

打開軟件新建一個工程 選擇chose 工程命名&#xff0c;選擇保存路徑&#xff0c;可以自己選擇&#xff0c;但是不要有中文路徑 默認的直接下一步 任意選一個下一步 點擊完成 之后是這個界面&#xff0c;點擊右下角的綠色三角形編譯一下 實驗內容 添加類 第一個是建立cpp和.h文件…

【NLP 53、投機采樣加速推理】

目錄 一、投機采樣 二、投機采樣改進&#xff1a;美杜莎模型 流程 改進 三、Deepseek的投機采樣 流程 Ⅰ、輸入文本預處理 Ⅱ、引導模型預測 Ⅲ、候選集篩選&#xff08;可選&#xff09; Ⅳ、主模型驗證 Ⅴ、生成輸出與循環 騙你的&#xff0c;其實我在意透了 —— 25.4.4 一、…

ffmpeg時間基與時間戳

時間基、時間戳 時間基&#xff1a;表示時間單位的分數&#xff0c;用來定義視頻或音頻流中時間的精度。其形式是一個分數&#xff0c;分子通常為 1&#xff0c;而分母則表示每秒的單位數。 時間戳&#xff1a;代表在時間軸里占了多少個格子&#xff0c;是特定的時間點。 時間…

激光加工中平面傾斜度的矯正

在激光加工中&#xff0c;加工平面的傾斜度矯正至關重要&#xff0c;直接影響加工精度和材料處理效果。以下是系統的矯正方法和步驟&#xff1a; 5. 驗證與迭代 二次測量&#xff1a;加工后重新檢測平面度&#xff0c;確認殘余誤差。 反饋優化&#xff1a;根據誤差分布修正補償…

算法刷題記錄——LeetCode篇(2.2) [第111~120題](持續更新)

更新時間&#xff1a;2025-04-04 算法題解目錄匯總&#xff1a;算法刷題記錄——題解目錄匯總技術博客總目錄&#xff1a;計算機技術系列博客——目錄頁 優先整理熱門100及面試150&#xff0c;不定期持續更新&#xff0c;歡迎關注&#xff01; 114. 二叉樹展開為鏈表 給你二…

C語言學習筆記-9

九、結構體 構造類型&#xff1a; 不是基本類型的數據結構也不是指針類型&#xff0c; 它是若干個相同或不同類型的數據構成的集合 結構體類型&#xff1a; 結構體是一種構造類型的數據結構&#xff0c;是一種或多種基本類型或構造類型的數據的集合。 1.結構體類型定義 定…

Test——BUG篇

目錄 一軟件測試的生命周期 二BUG 1概念 2描述Bug 3Bug級別 4Bug的生命周期 三與開發人員發生爭執怎么辦 ?編輯1先自省&#xff1a;是否Bug描述不清晰 2站在用戶角度考慮并拋出問題 3Bug定級有理有據 4不僅要提出問題&#xff0c;還要給出解決方案 5Bug評審 5.1…