前言
在計算機(jī)編程中,單元測試是一種軟件測試方法沥匈,通過該方法可以測試源代碼的各個單元功能是否適合使用聪黎。為代碼編寫單元測試有很多好處罕容,包括可以及早的發(fā)現(xiàn)代碼錯誤备恤,促進(jìn)更改,簡化集成锦秒,方便代碼重構(gòu)以及許多其它功能露泊。使用 Java
語言的朋友應(yīng)該用過或者聽過 Junit
就是用來做單元測試的,那么為什么我們還需要 Mockito 測試框架呢旅择?想象一下這樣的一個常見的場景惭笑,當(dāng)前要測試的類依賴于其它一些類對象時,如果用 Junit
來進(jìn)行單元測試的話生真,我們就必須手動創(chuàng)建出這些依賴的對象沉噩,這其實是個比較麻煩的工作,此時就可以使用 Mockito
測試框架來模擬那些依賴的類柱蟀,這些被模擬的對象在測試中充當(dāng)真實對象的虛擬對象或克隆對象川蒙,而且 Mockito
同時也提供了方便的測試行為驗證。這樣就可以讓我們更多地去關(guān)注當(dāng)前測試類的邏輯长已,而不是它所依賴的對象畜眨。
生成 Mock 對象方式
要使用 Mockito
,首先需要在我們的項目中引入 Mockito
測試框架依賴术瓮,基于 Maven
構(gòu)建的項目引入如下依賴即可:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
如果是基于 Gradle
構(gòu)建的項目康聂,則引入如下依賴:
testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'
使用 Mockito
通常有兩種常見的方式來創(chuàng)建 Mock
對象。
1胞四、使用 Mockito.mock(clazz) 方式
通過 Mockito
類的靜態(tài)方法 mock
來創(chuàng)建 Mock
對象早抠,例如以下創(chuàng)建了一個 List
類型的 Mock
對象:
List<String> mockList = Mockito.mock(ArrayList.class);
由于 mock
方法是一個靜態(tài)方法,所以通常會寫成靜態(tài)導(dǎo)入方法的方式撬讽,即 List<String> mockList = mock(ArrayList.class)
蕊连。
2、使用 @Mock 注解方式
第二種方式就是使用 @Mock
注解方式來創(chuàng)建 Mock
對象游昼,使用該方式創(chuàng)需要注意的是要在運行測試方法前使用 MockitoAnnotations.initMocks(this)
或者單元測試類上加上 @ExtendWith(MockitoExtension.class)
注解甘苍,如下所示代碼創(chuàng)建了一個 List
類型的 Mock
對象(PS: @BeforeEach 是 Junit 5 的注解,功能類似于 Junit 4 的 @Before 注解烘豌。
):
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
//@ExtendWith(MockitoExtension.class)
public class MockitoTest {
@Mock
private List<String> mockList;
@BeforeEach
public void beforeEach() {
MockitoAnnotations.initMocks(this);
}
}
驗證性測試
Mockito
測試框架中提供了 Mockito.verify
靜態(tài)方法讓我們可以方便的進(jìn)行驗證性測試载庭,比如方法調(diào)用驗證、方法調(diào)用次數(shù)驗證廊佩、方法調(diào)用順序驗證等囚聚,下面看看具體的代碼。
驗證方法單次調(diào)用
驗證方法單次調(diào)用的話直接 verify
方法后加上待驗證調(diào)用方法即可标锄,以下代碼的功能就是驗證 mockList
對象的 size
方法被調(diào)用一次顽铸。
/**
* @author mghio
* @date: 2020-05-28
* @version: 1.0
* @description:
* @since JDK 1.8
*/
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {
@Mock
List<String> mockList;
@Test
void verify_SimpleInvocationOnMock() {
mockList.size();
verify(mockList).size();
}
}
驗證方法調(diào)用指定次數(shù)
除了驗證單次調(diào)用,我們有時候還需要驗證一些方法被調(diào)用多次或者指定的次數(shù)料皇,那么此時就可以使用 verify
+ times
方法來驗證方法調(diào)用指定次數(shù)谓松,同時還可以結(jié)合 atLeast
+ atMost
方法來提供調(diào)用次數(shù)范圍星压,同時還有 never
等方法驗證不被調(diào)用等。
/**
* @author mghio
* @date: 2020-05-28
* @version: 1.0
* @description:
* @since JDK 1.8
*/
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {
@Mock
List<String> mockList;
@Test
void verify_NumberOfInteractionsWithMock() {
mockList.size();
mockList.size();
verify(mockList, times(2)).size();
verify(mockList, atLeast(1)).size();
verify(mockList, atMost(10)).size();
}
}
驗證方法調(diào)用順序
同時還可以使用 inOrder
方法來驗證方法的調(diào)用順序鬼譬,下面示例驗證 mockList
對象的 size
娜膘、add
和 clear
方法的調(diào)用順序。
/**
* @author mghio
* @date: 2020-05-28
* @version: 1.0
* @description:
* @since JDK 1.8
*/
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {
@Mock
List<String> mockList;
@Test
void verify_OrderedInvocationsOnMock() {
mockList.size();
mockList.add("add a parameter");
mockList.clear();
InOrder inOrder = inOrder(mockList);
inOrder.verify(mockList).size();
inOrder.verify(mockList).add("add a parameter");
inOrder.verify(mockList).clear();
}
}
以上只是列舉了一些簡單的驗證性測試优质,還有驗證測試方法調(diào)用超時以及更多的驗證測試可以通過相關(guān)官方文檔探索學(xué)習(xí)竣贪。
驗證方法異常
異常測試我們需要使用 Mockito
框架提供的一些調(diào)用行為定義,Mockito
提供了 when(...).thenXXX(...)
來讓我們定義方法調(diào)用行為巩螃,以下代碼定義了當(dāng)調(diào)用 mockMap
的 get
方法無論傳入任何參數(shù)都會拋出一個空指針 NullPointerException
異常贾富,然后通過 Assertions.assertThrows
來驗證調(diào)用結(jié)果。
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
@ExtendWith(MockitoExtension.class)
public class MockitoExceptionTest {
@Mock
public Map<String, Integer> mockMap;
@Test
public void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
when(mockMap.get(anyString())).thenThrow(NullPointerException.class);
assertThrows(NullPointerException.class, () -> mockMap.get("mghio"));
}
}
同時 when(...).thenXXX(...)
不僅可以定義方法調(diào)用拋出異常牺六,還可以定義調(diào)用方法后的返回結(jié)果颤枪,比如 when(mockMap.get("mghio")).thenReturn(21);
定義了當(dāng)我們調(diào)用 mockMap
的 get
方法并傳入?yún)?shù) mghio
時的返回結(jié)果是 21
。這里有一點需要注意淑际,使用以上這種方式定義的 mock
對象測試實際并不會影響到對象的內(nèi)部狀態(tài)畏纲,如下圖所示:
雖然我們已經(jīng)在 mockList
對象上調(diào)用了 add
方法,但是實際上 mockList
集合中并沒有加入 mghio
春缕,這時候如果需要對 mock
對象有影響盗胀,那么需要使用 spy
方式來生成 mock
對象。
public class MockitoTest {
private List<String> mockList = spy(ArrayList.class);
@Test
public void add_spyMockList_thenAffect() {
mockList.add("mghio");
assertEquals(0, mockList.size());
}
}
端點后可以發(fā)現(xiàn)當(dāng)使用 spy
方法創(chuàng)建出來的 mock
對象調(diào)用 add
方法后锄贼,mghio
被成功的加入到 mockList
集合當(dāng)中票灰。
與 Spring 框架集成
Mockito
框架提供了 @MockBean
注解用來將 mock
對象注入到 Spring
容器中,該對象會替換容器中任何現(xiàn)有的相同類型的 bean
宅荤,該注解在需要模擬特定bean
(例如外部服務(wù))的測試場景中很有用屑迂。如果使用的是 Spring Boot 2.0+
并且當(dāng)前容器中已有相同類型的 bean
的時候,需要設(shè)置 spring.main.allow-bean-definition-overriding
為 true
(默認(rèn)為 false
)允許 bean
定義覆蓋冯键。下面假設(shè)要測試通過用戶編碼查詢用戶的信息惹盼,有一個數(shù)據(jù)庫操作層的 UserRepository
,也就是我們等下要 mock
的對象惫确,定義如下:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
@Repository
public interface UserRepository {
User findUserById(Long id);
}
還有用戶操作的相關(guān)服務(wù) UserService
類手报,其定義如下所示:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
@Service
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findUserById(id);
}
}
在測試類中使用 @MockBean
來標(biāo)注 UserRepository
屬性表示這個類型的 bean
使用的是 mock
對象,使用 @Autowired
標(biāo)注表示 UserService
屬性使用的是 Spring
容器中的對象改化,然后使用 @SpringBootTest
啟用 Spring
環(huán)境即可掩蛤。
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
@SpringBootTest
public class UserServiceUnitTest {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
User expectedUser = new User(9527L, "mghio", "18288888880");
when(userRepository.findUserById(9527L)).thenReturn(expectedUser);
User actualUser = userService.findUserById(9527L);
assertEquals(expectedUser, actualUser);
}
}
Mockito 框架的工作原理
通過以上介紹可以發(fā)現(xiàn), Mockito
非常容易使用并且可以方便的驗證一些方法的行為陈肛,相信你已經(jīng)看出來了揍鸟,使用的步驟是先創(chuàng)建一個需要 mock
的對象 Target
,該對象如下:
public class Target {
public String foo(String name) {
return String.format("Hello, %s", name);
}
}
然后我們直接使用 Mockito.mock
方法和 when(...).thenReturn(...)
來生成 mock
對象并指定方法調(diào)用時的行為燥爷,代碼如下:
@Test
public void test_foo() {
String expectedResult = "Mocked mghio";
when(mockTarget.foo("mghio")).thenReturn(expectedResult);
String actualResult = mockTarget.foo("mghio");
assertEquals(expectedResult, actualResult);
}
仔細(xì)觀察以上 when(mockTarget.foo("mghio")).thenReturn(expectedResult)
這行代碼蜈亩,首次使用我也覺得很奇怪,when
方法的入?yún)⒕谷皇欠椒ǖ姆祷刂?mockTarget.foo("mghio")
前翎,覺得正確的代碼應(yīng)該是這樣 when(mockTarget).foo("mghio")
稚配,但是這個寫法實際上無法進(jìn)行編譯。既然 Target.foo
方法的返回值是 String
類型港华,那是不是可以使用如下方式呢道川?
Mockito.when("Hello, I am mghio").thenReturn("Mocked mghio");
結(jié)果是編譯通過,但是在運行時報錯:
從錯誤提示可以看出立宜,when
方法需要一個方法調(diào)用的參數(shù)冒萄,實際上它只需要 more
對象方法調(diào)用在 when
方法之前就行,我們看看下面這個測試代碼:
@Test
public void test_mockitoWhenMethod() {
String expectedResult = "Mocked mghio";
mockTarget.foo("mghio");
when("Hello, I am mghio").thenReturn(expectedResult);
String actualResult = mockTarget.foo("mghio");
assertEquals(expectedResult, actualResult);
}
以上代碼可以正常測試通過橙数,結(jié)果如下:
為什么這樣就可以正常測試通過尊流?是因為當(dāng)我們調(diào)用 mock
對象的 foo
方法時,Mockito
會攔截方法的調(diào)用然后將方法調(diào)用的詳細(xì)信息保存到 mock
對象的上下文中灯帮,當(dāng)調(diào)用到 Mockito.when
方法時崖技,實際上是從該上下文中獲取最后一個注冊的方法調(diào)用,然后把 thenReturn
的參數(shù)作為其返回值保存钟哥,然后當(dāng)我們的再次調(diào)用 mock
對象的該方法時迎献,之前已經(jīng)記錄的方法行為將被再次回放,該方法觸發(fā)攔截器重新調(diào)用并且返回我們在 thenReturn
方法指定的返回值腻贰。以下是 Mockito.when
方法的源碼:
該方法里面直接使用了 MockitoCore.when
方法吁恍,繼續(xù)跟進(jìn),該方法源碼如下:
仔細(xì)觀察可以發(fā)現(xiàn)播演,在源碼中并沒有用到參數(shù) methodCall
冀瓦,而是從 MockingProgress
實例中獲取 OngoingStubbing
對象,這個 OngoingStubbing
對象就是前文所提到的上下文對象写烤。個人感覺是 Mockito
為了提供簡潔易用的 API
然后才制造了 when
方法調(diào)用的這種“幻象”咕幻,簡而言之,Mockito
框架通過方法攔截在上下文中存儲和檢索方法調(diào)用詳細(xì)信息來工作的顶霞。
如何實現(xiàn)一個微型的 Mock 框架
知道了 Mockito
的運行原理之后肄程,接下來看看要如何自己去實現(xiàn)一個類似功能的 mock
框架出來,看到方法攔截
這里我相信你已經(jīng)知道了选浑,其實這就是 AOP
啊蓝厌,但是通過閱讀其源碼發(fā)現(xiàn) Mockito
其實并沒有使用我們熟悉的 Spring AOP
或者 AspectJ
做的方法攔截,而是通過運行時增強庫 Byte Buddy 和反射工具庫 Objenesis 生成和初始化 mock
對象的古徒。
現(xiàn)在拓提,通過以上分析和源碼閱讀可以定義出一個簡單版本的 mock
框架了,將自定義的 mock
框架命名為 imock
隧膘。這里有一點需要注意的是代态,Mockito
有一個好處是寺惫,它不需要進(jìn)行初始化,可以直接通過其提供的靜態(tài)方法來立即使用它蹦疑。在這里我們也使用相同名稱的靜態(tài)方法西雀,通過 Mockito
源碼:
很容易看出 Mockito
類最終都是委托給 MockitoCore
去實現(xiàn)的功能,而其只提供了一些面向使用者易用的靜態(tài)方法歉摧,在這里我們也定義一個這樣的代理對象 IMockCore
艇肴,這個類中需要一個創(chuàng)建 mock
對象的方法 mock
和一個給方法設(shè)定返回值的 thenReturn
方法,同時該類中持有一個方法調(diào)用詳情 InvocationDetail
集合列表叁温,這個類是用來記錄方法調(diào)用詳細(xì)信息的再悼,然后 when
方法僅返回列表中的最后一個 InvocationDetail
,這里列表可以直接使用 Java
中常用的 ArrayList
即可膝但,這里的 ArrayList
集合列表就實現(xiàn)了 Mockito
中的 OngoingStubbing
的功能冲九。
根據(jù)方法的三要素方法名
、方法參數(shù)
和方法返回值
很容易就可以寫出 InvocationDetail
類的代碼跟束,為了對方法在不同類有同名的情況區(qū)分娘侍,還需要加上類全稱字段和重寫該類的 equals
和 hashCode
方法(判斷是否在調(diào)用方法集合列表時需要根據(jù)該方法判斷),代碼如下所示:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
public class InvocationDetail<T> {
private String attachedClassName;
private String methodName;
private Object[] arguments;
private T result;
public InvocationDetail(String attachedClassName, String methodName, Object[] arguments) {
this.attachedClassName = attachedClassName;
this.methodName = methodName;
this.arguments = arguments;
}
public void thenReturn(T t) {
this.result = t;
}
public T getResult() {
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InvocationDetail<?> behaviour = (InvocationDetail<?>) o;
return Objects.equals(attachedClassName, behaviour.attachedClassName) &&
Objects.equals(methodName, behaviour.methodName) &&
Arrays.equals(arguments, behaviour.arguments);
}
@Override
public int hashCode() {
int result = Objects.hash(attachedClassName, methodName);
result = 31 * result + Arrays.hashCode(arguments);
return result;
}
}
接下來就是如何去創(chuàng)建我們的 mock
對象了泳炉,在這里我們也使用 Byte Buddy
和 Objenesis
庫來創(chuàng)建 mock
對象憾筏,IMockCreator
接口定義如下:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
public interface IMockCreator {
<T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList);
}
實現(xiàn)類 ByteBuddyIMockCreator
使用 Byte Buddy
庫在運行時動態(tài)生成 mock
類對象代碼然后使用 Objenesis
去實例化該對象。代碼如下:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
public class ByteBuddyIMockCreator implements IMockCreator {
private final ObjenesisStd objenesisStd = new ObjenesisStd();
@Override
public <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList) {
ByteBuddy byteBuddy = new ByteBuddy();
Class<? extends T> classWithInterceptor = byteBuddy.subclass(mockTargetClass)
.method(ElementMatchers.any())
.intercept(MethodDelegation.to(InterceptorDelegate.class))
.defineField("interceptor", IMockInterceptor.class, Modifier.PRIVATE)
.implement(IMockIntercepable.class)
.intercept(FieldAccessor.ofBeanProperty())
.make()
.load(getClass().getClassLoader(), Default.WRAPPER).getLoaded();
T mockTargetInstance = objenesisStd.newInstance(classWithInterceptor);
((IMockIntercepable) mockTargetInstance).setInterceptor(new IMockInterceptor(behaviorList));
return mockTargetInstance;
}
}
基于以上分析我們可以很容易寫出創(chuàng)建 mock
對象的 IMockCore
類的代碼如下:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
public class IMockCore {
private final List<InvocationDetail> invocationDetailList = new ArrayList<>(8);
private final IMockCreator mockCreator = new ByteBuddyIMockCreator();
public <T> T mock(Class<T> mockTargetClass) {
T result = mockCreator.createMock(mockTargetClass, invocationDetailList);
return result;
}
@SuppressWarnings("unchecked")
public <T> InvocationDetail<T> when(T methodCall) {
int currentSize = invocationDetailList.size();
return (InvocationDetail<T>) invocationDetailList.get(currentSize - 1);
}
}
提供給使用者的類 IMock
只是對 IMockCore
進(jìn)行的簡單調(diào)用而已花鹅,代碼如下:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
public class IMock {
private static final IMockCore IMOCK_CORE = new IMockCore();
public static <T> T mock(Class<T> clazz) {
return IMOCK_CORE.mock(clazz);
}
public static <T> InvocationDetail when(T methodCall) {
return IMOCK_CORE.when(methodCall);
}
}
通過以上步驟氧腰,我們就已經(jīng)實現(xiàn)了一個微型的 mock
框架了,下面來個實際例子測試一下刨肃,首先創(chuàng)建一個 Target
對象:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
public class Target {
public String foo(String name) {
return String.format("Hello, %s", name);
}
}
然后編寫其對應(yīng)的測試類 IMockTest
類如下:
/**
* @author mghio
* @date: 2020-05-30
* @version: 1.0
* @description:
* @since JDK 1.8
*/
public class IMockTest {
@Test
public void test_foo_method() {
String exceptedResult = "Mocked mghio";
Target mockTarget = IMock.mock(Target.class);
IMock.when(mockTarget.foo("mghio")).thenReturn(exceptedResult);
String actualResult = mockTarget.foo("mghio");
assertEquals(exceptedResult, actualResult);
}
}
以上測試的可以正常運行古拴,達(dá)到了和 Mockito
測試框架一樣的效果,運行結(jié)果如下:
上面只是列出了一些關(guān)鍵類的源碼真友,自定義 IMock
框架的所有代碼已上傳至 Github
倉庫 imock黄痪,感興趣的朋友可以去看看。
總結(jié)
本文只是介紹了 Mockito
的一些使用方法盔然,這只是該框架提供的最基礎(chǔ)功能桅打,更多高級的用法可以去官網(wǎng)閱讀相關(guān)的文檔,然后介紹了框架中 when(...).thenReturn(...)
定義行為方法的實現(xiàn)方式并按照其源碼思路實現(xiàn)了一個相同功能的簡易版的 imock 愈案。雖然進(jìn)行單元測試有很多優(yōu)點挺尾,但是也不可盲目的進(jìn)行單元測試,在大部分情況下只要做好對項目中邏輯比較復(fù)雜站绪、不容易理解的核心業(yè)務(wù)模塊以及項目中公共依賴的模塊的單元測試就可以了遭铺。
參考文章