在測試過程中,建立所需的模擬物件,模擬任何由 Spring 管理的 bean、或輸入參數、方法的返回值、拋出異常等,避免為了測試一個方法,建構了所有的相依賴物件, EP 12-2 增加了相依套件及採用 JUnit 5 單元測試來驗證產出結果。
Table of Contents
Toggle功能簡介
Mockito 是一個 Java mock 框架,主要是用來做模擬測試,可以透過 mock 方式建立一個假的對象去模擬真實行為,專注於本身測試的程式,提高單元測試的穩定性。
JUnit 5 Mockito
檔案目錄
+- build.gradle
+- src
+- main
+- java
+- org
+- ruoxue
+- spring_boot_168
+- sso
+- member
+- repository
+- MemberRepository.java
+- MemberRepositoryImpl.java
+- service
+- MemberService.java
+- MemberServiceImpl.java
Gradle
build.gradle
Spring Boot Starter Test
排除 JUnit 4 ,增加 JUnit 5 。
排除 Mockito 2.23.4,使用 4.8.1 。
排除 Byte Buddy 1.9.16,使用 1.12.18 。
plugins 增加 Spring Boot 、Dependency Management 。
修改完後,點右鍵,Gradle -> Refresh Gradle Project 。
buildscript {
group 'org.ruoxue.spring-boot-168'
version = '0.0.1-SNAPSHOT'
ext {
springBootVersion = '2.1.7.RELEASE'
mockitoVersion = '4.8.1'
bytebuddyVersion = '1.12.18'
}
}
plugins {
id 'java-library'
id 'eclipse'
id 'org.springframework.boot' version '2.1.7.RELEASE'
id 'io.spring.dependency-management' version '1.0.6.RELEASE'
}
dependencies {
testImplementation ("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") {
exclude group: 'junit', module: 'junit'
exclude group: 'org.mockito', module: 'mockito-core'
exclude group: 'net.bytebuddy', module: 'byte-buddy'
}
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
testImplementation "org.mockito:mockito-junit-jupiter:${mockitoVersion}"
testImplementation "net.bytebuddy:byte-buddy:${bytebuddyVersion}"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine"
}
test {
useJUnitPlatform()
}
實作
MemberRepository.java
Java Mockito 建立 Repository 介面,宣告 findName 方法,此時並沒有任何實作。
package org.ruoxue.spring_boot_168.sso.member.repository;
public interface MemberRepository {
String findName(String cid);
}
MemberRepositoryImpl.java
Java Mockito 實作方法,傳回 String 資料型別,在單元測試時,驗證這些返回值,是否有達到預期的標準。
package org.ruoxue.spring_boot_168.sso.member.repository;
public class MemberRepositoryImpl implements MemberRepository{
@Override
public String findName(String cid) {
return "player";
}
}
MemberService.java
Mockito Java 建立 Service 介面,宣告 findName 方法,此時並沒有任何實作。
package org.ruoxue.spring_boot_168.sso.member.service;
public interface MemberService {
String findName(String cid);
}
MemberServiceImpl.java
Mockito Java 注入 MemberRepository ,實作方法,傳回 String 資料型別。
package org.ruoxue.spring_boot_168.sso.member.service;
import org.ruoxue.spring_boot_168.sso.member.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
public class MemberServiceImpl implements MemberService {
@Autowired
private MemberRepository memberRepository;
@Override
public String findName(String cid) {
return memberRepository.findName(cid);
}
}
單元測試
Mockito Tutorial 新增單元測試,驗證是否符合預期,增加 @ExtendWith(SpringExtension.class)。
MemberServiceImplMockTest.java
package org.ruoxue.spring_boot_168.sso.member.service;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.ruoxue.spring_boot_168.sso.member.repository.MemberRepository;
@ExtendWith(SpringExtension.class)
public class MemberServiceImplMockTest {
@Mock
private MemberRepository memberRepository;
@InjectMocks
private MemberServiceImpl memberService;
@Test
public void findName() {
when(memberRepository.findName(anyString())).thenReturn("mock player");
String name = memberService.findName("ruoxue");
System.out.println(name);
assertEquals("mock player", name);
}
}
findName
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
mock player
故障排除
無法實例化 Mockito InjectMocks
執行 JUnit Test,拋出例外,發生錯誤,stack trace 如下:
org.mockito.exceptions.base.MockitoException:
Cannot instantiate @InjectMocks field named 'memberService'! Cause: the type 'MemberService' is an interface.
You haven't provided the instance at field declaration so I tried to construct the instance.
Examples of correct usage of @InjectMocks:
@InjectMocks Service service = new Service();
@InjectMocks Service service;
//and... don't forget about some @Mocks for injection :)
at org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener.initMocks(MockitoTestExecutionListener.java:68)
at org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener.prepareTestInstance(MockitoTestExecutionListener.java:53)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:246)
這是因為 MemberService 是介面,@InjectMocks 無法實例化成物件,所以改為 MemberServiceImpl 實作,修改完後,再次執行,就能順利運行。
// 修改前
@InjectMocks
private MemberService memberService;
// 修改後
@InjectMocks
private MemberServiceImpl memberService;
缺少 Mockito Mock
執行 JUnit Test,拋出例外,發生錯誤,stack trace 如下:
java.lang.NullPointerException
at org.ruoxue.spring_boot_168.sso.member.service.MemberServiceImplMockTest.findName(MemberServiceImplMockTest.java:23)
這是因為 MemberRepository 無建構成物件,要加上 @Mock 註解,使其實例化,修改完後,再次執行,就能順利運行。
// 修改前
private MemberRepository memberRepository;
// 修改後
@Mock
private MemberRepository memberRepository;
ByteBuddy 版本衝突
執行 JUnit Test,拋出例外,發生錯誤,stack trace 如下:
2022-11-17T09:03:34.493+0800 [main] ERROR TestContextManager#: - Caught exception while allowing TestExecutionListener [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@53f6fd09] to prepare test instance [org.ruoxue.spring_boot_168.sso.member.service.MemberServiceImplMockTest@5629510]
org.mockito.exceptions.base.MockitoException:
Failed to release mocks
This should not happen unless you are using a third-party mock maker
at org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener.initMocks(MockitoTestExecutionListener.java:68)
at org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener.prepareTestInstance(MockitoTestExecutionListener.java:53)
Caused by: java.lang.ClassNotFoundException: net.bytebuddy.utility.GraalImageCode
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
這是因為 ByteBuddy 與 Mockito 版本不匹配,所以在 build.gradle 設定排除舊版,引用新版,修改完後,再次執行,就能順利運行。
// 修改前
testImplementation ("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") {
exclude group: 'org.mockito', module: 'mockito-core'
}
testImplementation "org.mockito:mockito-core:4.8.1"
// 修改後
testImplementation ("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") {
exclude group: 'org.mockito', module: 'mockito-core'
exclude group: 'net.bytebuddy', module: 'byte-buddy'
}
testImplementation "org.mockito:mockito-core:4.8.1"
testImplementation "net.bytebuddy:byte-buddy:1.12.18"
心得分享
JUnit with Mockito 當隨著功能越來越多時,有時會發現單元測試很難寫,此時應該思考是否在模組切分上可以更加優化,考慮將一大段的程式碼,拆分為較小的類別、過程或方法,降低複雜度,Mockito Java 讓單元測試更加容易驗證。