簡介
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()等,匹配器不可以和常量混合使用