JUnit 5 Mockito - Spring Boot 168 EP 12-2

JUnit 5 Mockito – Spring Boot 168 EP 12-2

在測試過程中,建立所需的模擬物件,模擬任何由 Spring 管理的 bean、或輸入參數、方法的返回值、拋出異常等,避免為了測試一個方法,建構了所有的相依賴物件, EP 12-2 增加了相依套件及採用 JUnit 5 單元測試來驗證產出結果。

功能簡介

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 讓單元測試更加容易驗證。

發佈留言