Mockito MockedConstruction 概述
1. 概述
在編寫單元測試時,有時我們會遇到這樣的情況:在建構新物件時傳回模擬會很有用。例如,在測試具有緊密耦合的物件依賴性的遺留程式碼時。
在本教程中,我們將了解 Mockito 的一個相對較新的功能,它允許我們在構造函數呼叫上產生模擬。
要了解有關使用 Mockito 進行測試的更多信息,請查看我們全面的 Mockito 系列。
2. 依賴關係
首先,我們需要將mockito
依賴項加入我們的專案:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
如果我們使用的 Mockito 版本低於版本 5,那麼我們還需要明確添加 Mockito 的模擬生成器內聯相依性:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
3. 關於模擬構造函數呼叫的簡單介紹
一般來說,有些人可能會說,在編寫乾淨的物件導向程式碼時,我們不需要在建立物件時傳回模擬實例。這通常可能暗示我們的應用程式中存在設計問題或程式碼異味。
為什麼?首先,依賴幾個特定實現的類別可能會緊密耦合,其次,這幾乎總是會導致程式碼難以測試。理想情況下,類別不應該負責取得其依賴項,如果可能,它們應該從外部注入。
因此,我們是否可以重構程式碼以使其更易於測試,這始終值得研究。當然,這並不總是可能的,有時,我們需要在構造類別之後暫時替換它的行為。
這在幾種情況下可能特別有用:
- 測驗難以達到的場景 - 特別是當我們的被測類別具有複雜的物件層次結構時
- 測試與外部程式庫或框架的交互
- 使用遺留程式碼
在下面的部分中,我們將了解如何使用 Mockito 的MockConstruction
來應對其中一些情況,以便控制物件的創建並指定它們在構造時的行為。
4. 模擬建構函數
讓我們先建立一個簡單的Fruit
類,這將是我們第一個單元測試的重點:
public class Fruit {
public String getName() {
return "Apple";
}
public String getColour() {
return "Red";
}
}
現在讓我們繼續編寫測試,其中我們模擬對Fruit
類別進行的建構函式呼叫:
@Test
void givenMockedContructor_whenFruitCreated_thenMockIsReturned() {
assertEquals("Apple", new Fruit().getName());
assertEquals("Red", new Fruit().getColour());
try (MockedConstruction<Fruit> mock = mockConstruction(Fruit.class)) {
Fruit fruit = new Fruit();
when(fruit.getName()).thenReturn("Banana");
when(fruit.getColour()).thenReturn("Yellow");
assertEquals("Banana", fruit.getName());
assertEquals("Yellow", fruit.getColour());
List<Fruit> constructed = mock.constructed();
assertEquals(1, constructed.size());
}
}
在我們的範例中,我們首先檢查真實的Fruit
物件是否傳回所需的值。
現在,為了使模擬物件構造成為可能,我們將使用Mockito.mockConstruction()
方法。此方法採用非抽象 Java 類別來建構我們要模擬的結構。在本例中,是一個Fruit
類。
我們在 try-with-resources 區塊中定義它。這意味著當我們的程式碼在 try 語句中呼叫Fruit
物件的建構子時,它會傳回一個模擬物件。我們應該注意,構造函數不會在我們的作用域塊之外被 Mockito 模擬。
這是一個特別好的功能,因為它確保我們的模擬保持臨時狀態。眾所周知,如果我們在測試運行期間使用模擬構造函數調用,由於運行測試的並發和順序性質,這可能會對我們的測試結果產生不利影響。
5. 在另一個類別中模擬建構函數
更現實的場景是,當我們有一個正在測試的類別時,它會在內部創建一些我們想要模擬的物件。
通常,在被測試類別的建構函式內,我們可能會建立我們想要從測試中模擬的新物件的實例。在此範例中,我們將了解如何做到這一點。
讓我們先定義一個簡單的咖啡製作應用程式:
public class CoffeeMachine {
private Grinder grinder;
private WaterTank tank;
public CoffeeMachine() {
this.grinder = new Grinder();
this.tank = new WaterTank();
}
public String makeCoffee() {
String type = this.tank.isEspresso() ? "Espresso" : "Americano";
return String.format("Finished making a delicious %s made with %s beans", type, this.grinder.getBeans());
}
}
接下來,我們定義Grinder
類別:
public class Grinder {
private String beans;
public Grinder() {
this.beans = "Guatemalan";
}
public String getBeans() {
return beans;
}
public void setBeans(String beans) {
this.beans = beans;
}
}
最後,我們加入WaterTank
類別:
public class WaterTank {
private int mils;
public WaterTank() {
this.mils = 25;
}
public boolean isEspresso() {
return getMils() < 50;
}
//Getters and Setters
}
在這個簡單的範例中,我們的CoffeeMachine
在建造時創建了研磨機和水箱。我們有一個方法makeCoffee(),
它印出有關煮好的咖啡的消息。
現在,我們可以繼續寫一些測試:
@Test
void givenNoMockedContructor_whenCoffeeMade_thenRealDependencyReturned() {
CoffeeMachine machine = new CoffeeMachine();
assertEquals("Finished making a delicious Espresso made with Guatemalan beans", machine.makeCoffee());
}
在第一個測試中,我們檢查當我們不使用MockedConstruction,
我們的咖啡機會會回到內部的真實依賴項。
現在讓我們看看如何傳回這些依賴項的模擬:
@Test
void givenMockedContructor_whenCoffeeMade_thenMockDependencyReturned() {
try (MockedConstruction<WaterTank> mockTank = mockConstruction(WaterTank.class);
MockedConstruction<Grinder> mockGrinder = mockConstruction(Grinder.class)) {
CoffeeMachine machine = new CoffeeMachine();
WaterTank tank = mockTank.constructed().get(0);
Grinder grinder = mockGrinder.constructed().get(0);
when(tank.isEspresso()).thenReturn(false);
when(grinder.getBeans()).thenReturn("Peruvian");
assertEquals("Finished making a delicious Americano made with Peruvian beans", machine.makeCoffee());
}
}
在這個測試中,當我們呼叫Grinder
和WaterTank
的建構子時,我們使用mockConstruction
傳回模擬實例。然後,我們使用標準when
表示法指定這些模擬的期望。
這次,當我們執行測試時,Mockito 確保Grinder
和WaterTank
的建構子傳回具有指定行為的模擬實例,讓我們可以單獨測試makeCoffee
方法。
6. 處理建構子參數
另一個常見的用例是能夠處理帶有參數的建構函數。
值得慶幸的是, mockedConstruction
提供了一種機制,讓我們可以存取傳遞給建構函數的參數:
讓我們在WaterTank
中加入一個新的建構子:
public WaterTank(int mils) {
this.mils = mils;
}
同樣,我們也為 Coffee 應用程式新增一個新的建構子:
public CoffeeMachine(int mils) {
this.grinder = new Grinder();
this.tank = new WaterTank(mils);
}
最後,我們可以再增加一個測試:
@Test
void givenMockedContructorWithArgument_whenCoffeeMade_thenMockDependencyReturned() {
try (MockedConstruction<WaterTank> mockTank = mockConstruction(WaterTank.class,
(mock, context) -> {
int mils = (int) context.arguments().get(0);
when(mock.getMils()).thenReturn(mils);
});
MockedConstruction<Grinder> mockGrinder = mockConstruction(Grinder.class)) {
CoffeeMachine machine = new CoffeeMachine(100);
Grinder grinder = mockGrinder.constructed().get(0);
when(grinder.getBeans()).thenReturn("Kenyan");
assertEquals("Finished making a delicious Americano made with Kenyan beans", machine.makeCoffee());
}
}
這次,我們使用 lambda 表達式來處理帶有參數的WaterTank
構造函數。 lambda 接收模擬實例和建構上下文,讓我們可以存取傳遞給建構函數的參數。
然後,我們可以使用這些參數來設定getMils
方法所需的行為。
7. 更改預設的模擬行為
需要注意的是,對於方法,預設我們不會存根模擬回傳 null。我們可以將 Fruit 範例更進一步,讓模擬行為像真正的Fruit
實例一樣:
@Test
void givenMockedContructorWithNewDefaultAnswer_whenFruitCreated_thenRealMethodInvoked() {
try (MockedConstruction<Fruit> mock = mockConstruction(Fruit.class, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS))) {
Fruit fruit = new Fruit();
assertEquals("Apple", fruit.getName());
assertEquals("Red", fruit.getColour());
}
}
這次,我們將一個額外的參數MockSettings
傳遞給mockConstruction
方法,告訴它為我們沒有存根的方法創建一個模擬,該模擬的行為就像真實的Fruit
實例一樣。
八、結論
在這篇簡短的文章中,我們看到了幾個如何使用 Mockito 來模擬構造函數呼叫的範例。總而言之,Mockito 提供了一個優雅的解決方案,可以在當前線程和使用者定義的範圍內產生構造函數呼叫的模擬。
與往常一樣,本文的完整原始碼可以在 GitHub 上取得。