19 PowerMock學習筆記

1. PowerMock 介紹

1.1 簡介

在這裏插入圖片描述
PowerMock 主要圍繞着 Junit 測試框架TestNg 測試框架進行,其中在每個框架下面對所涉及的 Mock 框架僅僅支撐 EasyMock 和 Mockito,爲什麼要畫這個圖呢,是因爲PowerMock 對所依賴的 Jar 包非常的苛刻,如果出現某個依賴包的衝突或者不一致都會出現不能使用的情況,因此根據您的喜好和擅長,請選擇官網上提供的 PowerMock 套件

1.2 解決了什麼問題

在這裏插入圖片描述

1.3 如何獲得 PowerMock

您可以在 PowerMock 的官方網站免費獲得測試組件,我之前說過 PowerMock 對所依賴的 library 有些苛刻,因此最好還是老老實實用它提供的套件包,如下所示,您可以下載我紅色標註出來的測試套件。
在這裏插入圖片描述

或者使用maven

<properties>
	<powermock.version>1.5.6</powermock.version>
</properties>
<dependencies>
	<dependency>
		<groupId>org.powermock</groupId>
		<artifactId>powermock-module-junit4</artifactId>
		<version>${powermock.version}</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.powermock</groupId>
		<artifactId>powermock-api-mockito</artifactId>
		<version>${powermock.version}</version>
		<scope>test</scope>
	</dependency>
</dependencies>

2. PowerMock 入門

2.1 使用場景

在現實的軟件開發過程中,我們經常需要協同其他同事一起來完成某個模塊的功能開發,或者需要第三方資源,比如您需要一個短信網關,您需要一個數據庫,您需要一個消息中間件,當您進行測試依賴於這些資源的代碼時候,不可能在每次都具備相應的環境,這將是一個很不現實的問題,如果當您依賴於其他模塊而無法進行單元測試的時候,此時該模塊的質量風險就有兩個,第一是您所負責的代碼,第二是您所依賴的代碼,您所依賴您沒有辦法在很快的時間協調到資源,那麼您所負責的代碼由於不具備單元測試環境沒有辦法進行測試,很可能存在極大的風險,因此如何測試您的代碼,讓他的質量達到百分之百的可用,這就是 Mock 存在的必要,如果您還不能清楚這個場景,我們舉一個大家都耳熟能詳的例子來探討一下。
 
採用三層架構的方式實現某個 WEB 的請求到業務受理,已經變得非常成熟,甚至現在在更多的分層,更多的細化每一個層次的職責,儘量使某一個層次可以最大化的重用,減少耦合,下圖您應該非常熟悉了吧。

在這裏插入圖片描述
那麼請大家思考如下幾個問題:
1、如何保證各個層次模塊的代碼完全可用,在正常和臨界情況下?
2、您使用集成測試做單元測試麼?

很多人所謂的測試恐怕更多的是一種集成測試,也就是點擊頁面某個按鈕看看是否能夠順利執行,所依賴的各種資源都必須正常運行,如果出現問題就很難讓整個流程執行下去,如上圖所示,我們如何在沒有數據庫的時候能夠測試我們的 Service,沒有 Payment Gate的時候進行支付的測試等等,這纔是 Mock 要解決的問題,請注意是 Mock 而不是PowerMock(PowerMock 也是一種 Mock,但是他主要是解決其他 Mock 不能解決的問題)

2.2 PowerMock 之 HelloWorld

2.2.1 獲取所有員工的個數

我們現在有一個 Service 類,就是 EmployeeService,其中有一個方法需要獲取數據庫中僱員的數量,Service 代碼如下所示

public class EmployeeService {
	private EmployeeDao employeeDao;
	
	public EmployeeService(EmployeeDao employeeDao) {
		this.employeeDao = employeeDao; 
	}
	/**
	* 獲取所有員工的數量. * @return
	*/
	public int getTotalEmployee(){
		return employeeDao.getTotal();
	}
}

可以看到,創建 Service 的時候需要傳遞一個 EmployeeDao 這個類,也就是說 Service
依賴於 Persistence,如果想要測試 Service 就需要完全看 Persistence 的臉色,我們再來看看 Persistence 代碼,如下所示

public class EmployeeDao {
	public int getTotal(){
		throw new UnsupportedOperationException();
	} 
}

這個時候肯定調用不了 Dao,無法正常完成 Service 的測試,那麼爲什麼要在Persistence的方法拋出UnsupportedOperationException呢?目的就是告訴大家該方法可能由於某種原因(沒有完成,或者資源不存在等)無法爲 Service 服務,難道你不需要測
試 EmployeeService 麼?肯定要測試,那麼我們就硬着頭皮來寫測試用例吧

public class EmployeeServiceTest {
	@Test
	public void testGetTotalEmployee() {
		final EmployeeDao employeeDao = new EmployeeDao();
		final EmployeeService service = new EmployeeService(employeeDao);
		int total = service.getTotalEmployee();
		assertEquals(10, total);
	} 
}

上面的測試肯定會是以失敗收場
在這裏插入圖片描述
此刻拋出來的異常,就好像是此時此刻數據庫連接不上,問題現在很明顯,數據庫鏈接不通,我們無法測試 Service,難道真的就無計可施了麼?

public class EmployeeServiceTest {
	@Test
	public void testGetTotalEmployeeWithMock() {
		EmployeeDao employeeDao = PowerMockito.mock(EmployeeDao.class);
		PowerMockito.when(employeeDao.getTotal()).thenReturn(10);
		EmployeeService service = new EmployeeService(employeeDao);
		int total = service.getTotalEmployee();
		assertEquals(10, total);
	} 
}

當你再次運行時你會發現此時此刻運行通過,編寫一下上述的代碼我們先來有個簡單的認識,所謂 Mock 就是創建一個假的,Mock 那個對象就會創建一個假的該對象,此時該對象是一單純的白紙,需要你對其進行塗鴉,第二句話 when…then 語法就是您的塗鴉,您期望調用某個方法的時候返回某個您指定的值,最後的代碼已經非常熟悉了,此時此刻您已經完全讓EmployeeDao 根據你的意願來運行,所以想怎樣測試 EmployeeService 就怎樣測試。

2.2.2 創建員工

我們再來增加另外一個需求,就是創建一個 Employee,這就意味着我們需要分別在Service 和 Dao 中增加相應的兩個接口,請看代碼如下所示

Service 中的 CreateEmployee 方法

public void createEmployee(Employee employee) {
	employeeDao.addEmployee(employee);
}

再來看看 Dao 中的方法 addEmployee

public void addEmployee(Employee employee) {
	throw new UnsupportedOperationException();
}

至於addEmployee爲什麼拋出異常,在不多做解釋了,同樣如果針對createEmployee寫單元測試運行結果肯定死得很慘,因爲此時“數據庫資源不存在”,相信大家一定很清楚這一點,但是這不是本小節中所要講述的重點,重點在於 addEmployee 方法是一個 void類型的,也就是我們沒有辦法斷言想要的結果是否一致,而 mock 厚的 addEmployee 方法事實上是什麼都不會做的,此時我們該如何進行測試呢?
 
簡單思考一下我們其實只是想要知道 addEmployee 方法是否被調用過即可,當然我們可以假設他 add 成功或者失敗,這就根據您的 test case 來設定了,好了,有了這個概念之後我們來看看如何測試 void 方法,其實就是 mock 中一個很重要的概念 Verifying 了。

	@Test
	public void testCreateEmployee() {
		EmployeeDao employeeDao = PowerMockito.mock(EmployeeDao.class);
		Employee employee = new Employee();
		PowerMockito.doNothing().when(employeeDao).addEmployee(employee);
		EmployeeService service = new EmployeeService(employeeDao);
		service.createEmployee(employee);
		// verify the method invocation.
		Mockito.verify(employeeDao).addEmployee(employee);
	}

然後用 junit 運行肯定能夠通過,其中 Mockito.verify 主要用來校驗被 mock 出來的對象中的某個方法是否被調用,我們的 PowerMock helloworld 也到此結束了。

2.3 重點 API 解釋

2.3.1 Mock

Powermockito.mock() 方 法 主 要 是 根 據 class 創 建 一 個 對 應 的 mock 對 象 ,powermock 的創建方式可不像 easymock 等使用 proxy 的方式創建,他是會在你運行的過程中動態的修改 class 字節碼文件的形式來創建的。

2.3.2 Do…when…then

我們可以看到,每次當我們想給一個 mock 的對象進行某種行爲的預期時,都會使用do…when…then…這樣的語法,其實理解起來非常簡單:做什麼、在什麼時候、然後返回什麼。

2.3.3 Verify

當我們測試一個 void 方法的時候,根本沒有辦法去驗證一個 mock 對象所執行後的結果,因此唯一的方法就是檢查方法是否被調用,在後文中將還會專門來講解。

3. Mock Local Variable

3.1 有返回值

先來看看 Service 的代碼

public class EmployeeService {
	public int getTotalEmployee() {
		EmployeeDao employeeDao = new EmployeeDao();
		return employeeDao.getTotal();
	}
}

我們並沒有採用 hello world 中通過構造方法注入 dao 的方式,而是在方法內部 new出一個 EmployeeDao,我們通常都會寫這樣的代碼,根據之前的經驗,調用肯定是失敗的,而要測試這樣的代碼 EasyMock,Mockito 等顯然力不從心,這個時候優勢就很明顯的體現出來了,請看下面的測試代碼

@RunWith(PowerMockRunner.class)
@PrepareForTest(EmployeeService.class)
public class EmployeeServiceTest {
	/**
	 * 用傳統的方式肯定測試失敗。
	 */
	@Test
	public void testGetTotalEmployee() {
		EmployeeService service = new EmployeeService();
		int total = service.getTotalEmployee();
		assertEquals(10, total);
	}

	/**
	 * 採用 PowerMock 進行測試
	 */
	@Test
	public void testGetTotalEmployeeWithMock() {
		EmployeeDao employeeDao = PowerMockito.mock(EmployeeDao.class);
		try {
			PowerMockito.whenNew(EmployeeDao.class).withNoArguments().thenReturn(employeeDao);
			PowerMockito.when(employeeDao.getTotal()).thenReturn(10);
			EmployeeService service = new EmployeeService();
			int total = service.getTotalEmployee();
			assertEquals(10, total);
		} catch (Exception e) {
			fail("測試失敗.");
		}
	}
}

運行上面的代碼可以很明顯的看到其中傳統的方式是失敗的,而通過 Mock 方式測試的是成功的,當然您可能會非常驚訝 PowerMock 盡然有如此強大的能力,以至於可以Mock 出局部變量,那麼同樣我們再來看看局部變量的 void 方法該如何進行測試。

3.2 局部變量的 void 方法

同樣在 Service 中增加一個添加 Employee 的方法

	public void createEmployee(Employee employee) {
		EmployeeDao employeeDao = new EmployeeDao();
		employeeDao.addEmployee(employee);
	}

我們該如何測試這個方法呢?同樣的如果採用之前的傳統調用方式肯定會出現錯誤,我們只能採用 PowerMock 的方式來進行測試,測試代碼如下所示

	@Test
	public void testCreateEmployee() {
		EmployeeService service = new EmployeeService();
		Employee employee = new Employee();
		service.createEmployee(employee);
	}

	@Test
	public void testCreateEmployeeWithMock() {
		EmployeeDao employeeDao = PowerMockito.mock(EmployeeDao.class);
		try {
			PowerMockito.whenNew(EmployeeDao.class).withNoArguments().thenReturn(employeeDao);
			Employee employee = new Employee();
			EmployeeService service = new EmployeeService();
			service.createEmployee(employee);
			Mockito.verify(employeeDao).addEmployee(employee);
		} catch (Exception e) {
			fail();
		}
	}

運行上面的兩個測試用例,你會發現其中第一個失敗,第二個運行成功,對於測試 void返回類型的方法,同樣我們做的只能是判斷他是否被調用

3.3 @RunWith 和@PrepareForTest 介紹

其中**@RunWith** 就是顯式的告訴 Junit 使用某個指定的 Runner 來運行 Test Case,我們使用了 PowerMockRunner 來運行我們的測試用例,如果不指定的話我們就默認使用的是 Junit 提供的 Runner,先來看看下面這張圖,瞭解一下 Runner 家族的成員
在這裏插入圖片描述
由於我們在測試EmployeeService的時候需要改變EmployeeDao的創建行爲,通過這個的語句PowerMockito.whenNew(EmployeeDao.class).withNoArguments().thenReturn(
employeeDao);就可以看得出來,因此就需要了解另外一個註解@PrepareForTest,這個
註解是告訴PowerMock爲我提前準備一個xxx的class,根據我測試預期的行爲去準備,根據這段粗糙的文字描述,我們再來詳細的整理一下。

在測試EmployeeService的時候,因爲涉及到了局部變量的創建,我們必須讓他創建成爲我們想要的Mock對象,因此需要有一個錄製的過程PowerMockito.whenNew( EmployeeDao.class).withNoArguments().thenReturn(employeeDao); 這 句 話 就 是 告 訴Power Mock錄製一個這樣的構造行爲,等我接下來用的時候你就把之前mock的東西給我, 而@PrepareForTest是爲PowerMock的Runner提前準備一個已經根據某種預期改變過的class,在這個例子中就是EmployeeService,如果大家使用過Jacoco統計過Java測試代碼的覆蓋率就會發現一個很惱火的問題,那就是不管你寫了多少個EmployeeService的測試用例,只要你對其進行了PrepareForTest之後,代碼覆蓋率仍然是0,爲什麼會這樣呢,因爲經過了PrepareForTest的處理之後,PowerMock框架會通過類加載器的方式改變EmployeeService的行爲,如何改變呢?當然是產生了一個新的類在運行的過程中,新的類裏面但凡碰到要創建EmployeeDao的時候都會將提前錄製好的行爲重播出來,給出一個Mock對象。

4. Mock Static

有時候我們寫代碼的時候喜歡編寫已經寫工具類,工具類一般都提供了大量的便捷的類方法(static)供調用者使用,在本章中我們來看看如何進行靜態方法的測試

4.1 問題場景

同樣 Service 還是兩個接口,第一個是查詢僱員數量,第二個是創建一個新的僱員,代碼如下所示

public class EmployeeService {
	public int getEmployeeCount() {
		return EmployeeUtils.getEmployeeCount();
	}

	public void createEmployee(Employee employee) {
		EmployeeUtils.persistenceEmployee(employee);
	}
}

相比這樣的代碼在前文中已經看得非常多了,重點是其中的一個工具類 EmployeeUtils,同樣來看看它的代碼

public class EmployeeUtils {
	public static int getEmployeeCount() {
		throw new UnsupportedOperationException();
	}

	public static void persistenceEmployee(Employee employee) {
		throw new UnsupportedOperationException();
	}
}

4.2 單元測試

上面的兩個類代碼都非常簡單,但是寫一個專門針對 EmployeeService 的單元測試類,你會發現運行根本不能通過

public class EmployeeServiceTest {
	@Test
	public void testGetEmployeeCount() {
		final EmployeeService employeeService = new EmployeeService();
		int count = employeeService.getEmployeeCount();
		assertEquals(10, count);
	}

	@Test
	public void testCreateEmployee() {
		final EmployeeService employeeService = new EmployeeService();
		Employee employee = new Employee();
		employeeService.createEmployee(employee);
		assertTrue(true);
	}
}

4.3 使用 Mock

@RunWith(PowerMockRunner.class)
@PrepareForTest(EmployeeUtils.class)
public class EmployeeServiceTest {
	@Test
	public void testGetEmployeeCountWithMock() {
		PowerMockito.mockStatic(EmployeeUtils.class);
		PowerMockito.when(EmployeeUtils.getEmployeeCount()).thenReturn(10);
		final EmployeeService employeeService = new EmployeeService();
		int count = employeeService.getEmployeeCount();
		assertEquals(10, count);
	}

	@Test
	public void testCreateEmployeeWithMock() {
		PowerMockito.mockStatic(EmployeeUtils.class);
		Employee employee = new Employee();
		PowerMockito.doNothing().when(EmployeeUtils.class);
		final EmployeeService employeeService = new EmployeeService();
		employeeService.createEmployee(employee);
		PowerMockito.verifyStatic();
	}
}

運行上述的單元測試,你就會得到正確的測試結果和預期,首先我們讓 PowerMock在運行之前準備 EmployeeUtils 類,並且我們採用了 mockstatic 的方法,其中這個有別於 mock 非靜態的類,然後其他的操作就會完全一樣了

5. Verifying

Verifying 是一個非常強大的測試工具,不僅在 powermock 中使用,就連 easymock,
mockito 等框架都會使用,他的目的是爲了檢查某個被測試的方法是否順利的被調用了

5.1 使用場景

假設我們有一個這樣的業務接口,傳遞一個 Employee 實例參數,然後通過 DAO 查詢該 Employee 是否在對應的數據庫中存在,如果不存在則創建一個 Employee 數據,否則就更新原來的 Employee
在這裏插入圖片描述

5.2 業務代碼

EmployeeDao.java 代碼如下所示

public class EmployeeDao {
	/**
	 * @param employee
	 */
	public void saveEmployee(Employee employee) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @param employee
	 */
	public void updateEmploye(Employee employee) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @param employee
	 * @return
	 */
	public long getCount(Employee employee) {
		throw new UnsupportedOperationException();
	}
}

同樣爲了模擬 DAO 比較難以測試,我們依舊拋出了運行時異常,好了,再來看一看Service 中是如何調用的吧

public class EmployeeService {
	public void saveOrUpdate(Employee employee) {
		final EmployeeDao employeeDao = new EmployeeDao();
		long count = employeeDao.getCount(employee);
		if (count > 0)
			employeeDao.updateEmployee(employee);
		else
			employeeDao.saveEmployee(employee);
	}
}

在開始編寫測試代碼之前,我們先來思考一下,其中 Service 中的 saveOrUpdate 方法是用了 EmployeeDao,並且作爲一個局部變量被實例化,因此我們必須採用 PowerMock的方法進行測試,但是最重要的是由於其中多了一層邏輯判斷,這給我們的斷言帶來了一定的難度,由於不能真正的連接數據庫,所以沒有辦法產生真實的數據,所以我們斷言的依據是什麼?是新增了一條數據還是更改了一條記錄呢?

顯然採用傳統的方式我們更本沒有辦法做到這一點,但是 Verifying 卻可以讓我們很容易的應對這樣一個場景,首先我們不難看出,當 count 大於 0 的時候我們就需要執行更新操作,當 count 小於等於 0 的時候我們就需要執行新增的操作,那麼我們就可以通過驗證DAO 中所涉及的方法是否被調用來進行驗證了,如果這麼說您還是不能理解,接着看下面的兩個測試用例

5.3 測試代碼

@RunWith(PowerMockRunner.class)
@PrepareForTest(EmployeeService.class)
public class EmployServiceTest {
	@Test
	public void testSaveOrUpdateCountLessZero() {
		try {
			EmployeeDao employeeDao = PowerMockito.mock(EmployeeDao.class);
			PowerMockito.whenNew(EmployeeDao.class).withNoArguments().thenReturn(employeeDao);
			Employee employee = new Employee();
			PowerMockito.when(employeeDao.getCount(employee)).thenReturn(0L);
			EmployeeService employeeService = new EmployeeService();
			employeeService.saveOrUpdate(employee);
			Mockito.verify(employeeDao).saveEmployee(employee);
			Mockito.verify(employeeDao, Mockito.never()).updateEmploye(employee);
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		}
	}

	@Test
	public void testSaveOrUpdateCountMoreThanZero() {
		EmployeeDao employeeDao = PowerMockito.mock(EmployeeDao.class);
		try {
			PowerMockito.whenNew(EmployeeDao.class).withNoArguments().thenReturn(employeeDao);
			Employee employee = new Employee();
			PowerMockito.when(employeeDao.getCount(employee)).thenReturn(1L);
			EmployeeService employeeService = new EmployeeService();
			employeeService.saveOrUpdate(employee);
			Mockito.verify(employeeDao).updateEmploye(employee);
			Mockito.verify(employeeDao, Mockito.never()).saveEmployee(employee);
		} catch (Exception e) {
			fail();
		}
	}
}

verify 方法的使用,我們根據 getCount 的返回的結果去驗證對應的方法是否被調用了,從而來測試我們的 service 是否邏輯運行正確當然 Verify 的所涉及的方法還不至這些,其中有很多的 verification modes

5.4 Verifying 其他 API

Mockito.verify(employeeDao, Mockito.never()).saveEmployee(employee);
Mockito.verify(employeeDao,Mockito.atLeastOnce()).saveEmployee(employee);
Mockito.verify(employeeDao,Mockito.times(1)).saveEmployee(employee);
Mockito.verify(employeeDao,Mockito.atMost(1)).saveEmployee(employee);
Mockito.verify(employeeDao,Mockito.atLeast(1)).saveEmployee(employee);

6. Mock final

是使用 PowerMock 測試一下某個 final 修飾的 class 或 者 method,但是爲了更加有說服力,本章會引入 easymock 作爲對比, 看看 easymock 是否能夠 mock 一個 final修飾的class 或者 method

6.1 業務代碼

其中業務代碼和之前的業務代碼沒有什麼不一樣,都是一個 service 調用其中的一個dao,但是這個 dao class 是被 final 修飾的,也就是說他是不能被繼承的,我們看一下代
碼吧

Service 代碼

public class EmployeeService {
	private EmployeeDao employeeDao;

	public EmployeeService(EmployeeDao employeeDao) {
		this.employeeDao = employeeDao;
	}

	public void createEmployee(Employee employee) {
		employeeDao.insertEmployee(employee);
	}
}

Dao 的代碼如下

final public class EmployeeDao {
	public boolean insertEmployee(Employee employee) {
		throw new UnsupportedOperationException();
	}
}

Service 代碼沒有什麼,重點看一下 DAO 的代碼,修飾符多了一個 final 類型,同時爲
了能夠方便 EasyMock 進行測試,我將 EmployeeDao 在 EmployeeService 中通過了構造方法傳入(因爲 EasyMock 不能在 mock 方法內部的變量)

6.2 EasyMock 測試

public class EmployeeServiceEasyMockTest {
	@Test
	public void test() {
		EmployeeDao employeeDao = EasyMock.createMock(EmployeeDao.class);
		Employee employee = new Employee();
		EasyMock.expect(employeeDao.insertEmployee(employee)).andReturn(true);
		EasyMock.replay(employeeDao);
		EmployeeService employeeService = new EmployeeService(employeeDao);
		employeeService.createEmployee(employee);
		EasyMock.verify(employeeDao);
	}
}

執行上面的單元測試,你會發現運行出現了錯誤,但是當您 將 EmployeeDao 的 final 修飾符去 掉之後就會運 行成功,由此 可見EasyMock 是通過代理的方式實現(繼承代理)產生一個新的 Mock 對象的,同樣 final 修飾的方法也會不適合 EasyMock 的場景

6.3 PowerMock 測試

@RunWith(PowerMockRunner.class)
@PrepareForTest(EmployeeDao.class)
public class EmployeeServicePowerMockTest {
	@Test
	public void test() {
		EmployeeDao employeeDao = PowerMockito.mock(EmployeeDao.class);
		Employee employee = new Employee();
		PowerMockito.when(employeeDao.insertEmployee(employee)).thenReturn(true);
		EmployeeService employeeService = new EmployeeService(employeeDao);
		employeeService.createEmployee(employee);
		Mockito.verify(employeeDao).insertEmployee(employee);
	}
}

7. Mock constructors

有些時候我們需要在類的構造函數中傳遞某些參數進去,這也是非常常見的一種編碼習慣,這個時候我們應該如何去 Mock 這樣的類呢?

7.1 使用場景

舉個例子,DAO 的構造需要傳遞兩個參數,其中一個是 boolean 類型標識是否懶加載,另外一個是 enumeration 類型,標識採用何種數據庫方言,當然有可能構造該 DAO 需要很重的資源,並且在我們的單元測試環境下不一定能夠很容易的獲得,因此需要 mock

7.2 業務代碼

我們先來看看 DAO 的代碼

public class EmployeeDao {
	public enum Dialect {
		MYSQL, ORACLE
	}

	public EmployeeDao(boolean lazy, Dialect dialect) {
		throw new UnsupportedOperationException();
	}

	public void insertEmploye(Employee employee) {
		throw new UnsupportedOperationException();
	}
}

再來看看 Service 的代碼

public class EmployeeService {
	public void createEmployee(final Employee employee) {
		EmployeeDao employeeDao = new EmployeeDao(false, Dialect.MYSQL);
		employeeDao.insertEmploye(employee);
	}
}

7.3 PowerMock 測試

@RunWith(PowerMockRunner.class)
@PrepareForTest(EmployeeService.class)
public class EmployeeServiceTest {
	@Test
	public void test() {
		EmployeeDao employeeDao = mock(EmployeeDao.class);// (1)
		try {
			whenNew(EmployeeDao.class).withArguments(false, MYSQL).thenReturn(employeeDao);// (2)
			EmployeeService employeeService = new EmployeeService();
			Employee employee = new Employee();
			employeeService.createEmployee(employee);
			Mockito.verify(employeeDao).insertEmploye(employee);// (3)
		} catch (Exception e) {
			fail();
		}
	}
}

(1)我們首先 mock 了一個 EmployeeDao 實例
(2)我們用 whenNew 語法,並且委以相關的參數,注意這裏的參數必須和 Service中的參數一致,否則在 Service 中還會繼續創建一個新的 EmployeeDao 實例。
(3)我們驗證了方法的調用。

7.4 whenNew 語法

我們在上面的 PowerMock 測試用例中看到了一個新的語法那就是 whenNew,其實我們在前面的章節中都有涉獵該語法,這個語法的意思是當碰到 new 這個關鍵字時,返回某個被 Mock 的對象而不是讓真的 new 行爲發生,回想我們在 Service 中 new 一個新的EmployeeDao,如果我們不提前準備這個 new 行爲,那麼他會在運行期創建一個新的EmployeeDao 實例,此時我們 Mock 出來的實例將會不起任何作用的,並且對應的參數一定要一致,如果您不能確保參數的一致性,那就是用 withAnyArguments,當然還有對應的很多 whenNew 語法,我們來一起看一下。
在這裏插入圖片描述

8. Arguments Matcher

Arguments Matcher 是一個比較強大的 Mock 接口,其實這並不是 PowerMock 的原創,其實在 EasyMock,Mockito 中均有相關的支持,同時它也是一個比較靈活的 Mock手段

8.1 使用場景

有些時候我們 Mock 一個對象的時候,需要根據不同的參數返回不同的內容,雖然Mock 的對象是假的,但是我們還是希望能夠這樣做,做一些臨界值的測試,並且得到相關的預期結果,好了,我們先來看看業務代碼,其中業務代碼也比較簡單,我們從最基本的Controller 開始,然後通過 Controller 調用相應的 Service。

8.2 業務代碼

Controller 的代碼

public class EmployeeController {
	public String getEmail(final String userName) {
		EmployeeService employeeService = new EmployeeService();
		String email = employeeService.findEmailByUserName(userName);
		return email;
	}
}

Service 的代碼如下

public class EmployeeService {
	public String findEmailByUserName(String userName) {
		throw new UnsupportedOperationException();
	}
}

根據以前的知識,我們只能 Mock 一個 Service,並且能夠預先設定一個返回結果,但是我們在編寫代碼的時候,提前會根據輸入的參數獲得不同的返回值,比如參數爲 Null 或者一個不存在的結果等等。總而言之,我們就像根據參數的不同而獲得不同的返回結果。

8.3 PowerMock 測試

public class EmployeeControllerTest {
	@Test
	public void testGetEmail() {
		EmployeeService employeeService = mock(EmployeeService.class);
		when(employeeService.findEmailByUserName(Mockito.argThat(newArgumentMatcher<String>(){
			@Override
			public boolean matches(Object argument) {
				String arg = (String)argument;
				if(arg.startsWith("xx")||arg.startsWith("xxx"))
				return true;
				else
				throw new RuntimeException();
			}
		}))).thenReturn("[email protected]");
		
		try {
			whenNew(EmployeeService.class).withAnyArguments().thenReturn(employeeService);
			EmployeeController controller = new EmployeeController();
			String email = controller.getEmail("xxx");
			assertEquals("[email protected]",email);
			controller.getEmail("liudehua");
			fail("should not process to here.");
		} catch (Exception e) {
		assertTrue(e instanceof RuntimeException);
		} 
	}
}

上面的這個 PowerMock 測試代碼,我們通過實現一個匿名 ArgumentMatcher 類,然後就實現了根據不同參數獲得不同的返回結果預期,這樣我們就可以少些很多when…thenReturn

9. Answer interface

其實 Answer 接口的作用和 Arguments Matcher 比較類似,但是它比 Arguments Matcher 更加強大,我們還是根據上一章節中的那個示例然後來說明 Answer 的使用方法。

9.1 使用場景

使用場景在這裏還是羅嗦一下吧,當用戶名是 xx或者 xxx的時候能夠返回[email protected] , 當 用 戶 名 是 liudehua 或 者 ldh 的 時 候 返 回 的 是[email protected],如果用戶名是其他則拋出無法找到的異常。

9.2 業務代碼

Service 代碼

public class EmployeeService {
	public String findEmailByUserName(String userName) {
		throw new UnsupportedOperationException();
	}
}

Controller 代碼

public class EmployeeController {
	public String getEmail(final String userName) {
		EmployeeService employeeService = new EmployeeService();
		String email = employeeService.findEmailByUserName(userName);
		return email;
	}
}

9.3 PowerMock 測試

@RunWith(PowerMockRunner.class)
@PrepareForTest(EmployeeController.class)
public class EmployeeControllerTest {
	@Test
	public void testGetEmail() {
		EmployeeService employeeService = PowerMockito.mock(EmployeeService.class);
		PowerMockito.when(employeeService.findEmailByUserName(Mockito.anyString())).then(new Answer<String>(){
			@Override
			public String answer(InvocationOnMock invocation) throws
			Throwable {
				String argument = (String) invocation.getArguments()[0];
				if("xx".equals(argument))
					return "[email protected]";
					else if("liudehua".equals(argument))
					return "[email protected]";
					throw new NullPointerException();
			}
		});
		
		try {
			PowerMockito.whenNew(EmployeeService.class).withNoArguments().thenRe
			turn(employeeService);
			EmployeeController controller = new EmployeeController();
			String email = controller.getEmail("xx");
			assertEquals("[email protected]",email);
			email = controller.getEmail("liudehua");
			assertEquals("[email protected]",email);
			email = controller.getEmail("JackChen");
			fail("should not process to here.");
		} catch (Exception e) {
			assertTrue(e instanceof NullPointerException);
		} 
		}
}

9.4 answer 接口中參數 InvocationOnMock

invocation.getArguments();1)
invocation.callRealMethod();2)
invocation.getMethod();3)
invocation.getMock();4

(1)獲取 mock 方法中傳遞的入參
(2)獲取是那個真實的方法調用了該 mock 接口
(3)獲取是那麼 mock 方法被調用了
(4)獲取被 mock 之後的對象

10. Mocking with spies

10.1 使用場景

如果某個對象是 mock 產生的,那麼他什麼都不會做,除非你對其做了when…thenReturn 這樣的操作,否則所mock的對象調用任何方法,什麼都不會做的,如果您沒有做過類似的測試,我們在這裏一起來看看。

public class FileService {
	public void write(final String text) {
		BufferedWriter bw = null;
		try {
			bw = new BufferedWriter(new FileWriter(System.getProperty("user.dir") + "/xx.txt"));
			bw.write(text);
			bw.flush();
			System.out.println("content write successfully.");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (bw != null)
				try {
					bw.close();
				} catch (IOException e) {
					// be quietly
				}
		}
	}
}

上面的代碼其實完全不可以用 mock 的方式來測試,但是爲了說明我剛纔的理論,我們來寫一個測試用例來看一看

public class FileServiceTest {
	@Test
	public void testWrite() {
		FileService fileService = PowerMockito.mock(FileService.class);
		fileService.write("liudehua");
	}
}

運行上面的測試用例,你會發現根本沒有生成一個 xx.txt 文件,也就意味着被 mock 的 class 是個徹頭徹尾的假對象,什麼都幹不了的。

10.2 PowerMock 測試

我們採用 spy 的方式 mock 一個對象,然後再調用其中的某個方法,他就會根據真實
class 的所提供的方法來調用,好了,我們再來寫一個 spy 的測試

	@Test
	public void testWriteSpy() {
		FileService fileService = PowerMockito.spy(new FileService());
		fileService.write("liudehua");
		File file = new File(System.getProperty("user.dir") + "/xx.txt");
		assertTrue(file.exists());
	}

運行上面的測試用例,你會發現生成了 xx.txt 文件,並且裏面有 liudehua字樣。

10.3 何時使用 Spy

Spy 是一個特別有意思的 API,他能讓你 mock 一個對象,並且只 mock 個別方法的行爲,保留對某些方法原始的業務邏輯,根據具體情況選擇使用。

11. Mocking private methods

單純就測試 private 修飾的方法而言,這個非常有爭議,有人說測試 private 方法採用反射的方式會破壞 class 的封裝性,有人說既然是單元測試需要面面俱到,在互聯網上爭論
都是比較激烈的,那方都沒有說服另外一方,我個人也是比較贊成不要通過反射的方式去測試一個私有方法,如果私有方法寫得好,對調用處的代碼寫好單元測試就會完全覆蓋私有方法的邏輯。

但是本章中所要體現出來的場景還真的需要去 mock 一個 private 方法,好了,來看看
最後一個 PowerMock 的功能吧。

11.1 使用場景

假設我們有一個類,其中有一個方法 A 是公有方法,但是他需要依賴一個私有方法 B,但是其中的方法 B 是一個很難在單元測試中真實模擬,所以我們需要 mock 私有方法行
爲,好了我們同樣看一看業務代碼,然後再通過 PowerMock 的方式來進行一下測試

11.2 業務代碼

public class EmployeeService {
	public boolean exist(String userName) {
		checkExist(userName);
		return true;
	}

	private void checkExist(String userName) {
		throw new UnsupportedOperationException();
	}
}

11.3 PowerMock 測試

上面的業務代碼中我們發現如果要測試 exist 方法,肯定需要實現 mock 出來checkExist 的行爲,否則真的沒有辦法對 exist 進行測試。

@RunWith(PowerMockRunner.class)
@PrepareForTest(EmployeeService.class)
public class EmployeeServiceTest {
	@Test
	public void testExist() {
		try {
			EmployeeService employeeService = PowerMockito.spy(new EmployeeService());
			PowerMockito.doNothing().when(employeeService, "checkExist", "xx");
			boolean result = employeeService.exist("xx");
			assertTrue(result);
			PowerMockito.verifyPrivate(employeeService).invoke("checkExist","xx");
		} catch (Exception e) { 
			e.printStackTrace();
			fail();
		} 
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章