【實戰技巧】使用(或不使用)Spring Boot進行模擬

前言:
Mockito是一個非常流行的庫,可以支持測試。它允許我們用“模擬”代替真實的對象,即用非真實的對象以及可以在測試中控制其行為的對象。

本文簡要介紹了Mockito和Spring Boot與之集成的方式和原因。

被測系統

本文測試的系統將是Spring REST控制器,該控制器接受將資金從一個帳戶轉移到另一個帳戶的請求:

<code>@RestController
@RequiredArgsConstructor
public class SendMoneyController {

private final SendMoneyUseCase sendMoneyUseCase;

@PostMapping(path = "/sendMoney/{sourceAccountId}/{targetAccountId}/{amount}")
ResponseEntity sendMoney(
@PathVariable("sourceAccountId") Long sourceAccountId,
@PathVariable("targetAccountId") Long targetAccountId,
@PathVariable("amount") Integer amount) {

SendMoneyCommand command = new SendMoneyCommand(
sourceAccountId,
targetAccountId,
amount);

boolean success = sendMoneyUseCase.sendMoney(command);

if (success) {
return ResponseEntity
.ok()
.build();
} else {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.build();
}
}

}
/<code>

控制器將輸入傳遞給具有SendMoneyUseCase單個方法的接口實例:

<code>public interface SendMoneyUseCase {

boolean sendMoney(SendMoneyCommand command);

@Value
@Getter
@EqualsAndHashCode(callSuper = false)
class SendMoneyCommand {

private final Long sourceAccountId;
private final Long targetAccountId;
private final Integer money;

public SendMoneyCommand(
Long sourceAccountId,
Long targetAccountId,
Integer money) {
this.sourceAccountId = sourceAccountId;
this.targetAccountId = targetAccountId;
this.money = money;
}
}

}
/<code>

最後,我們有一個實現SendMoneyUseCase接口的虛擬服務:

<code>@Slf4j
@Component
public class SendMoneyService implements SendMoneyUseCase {

public SendMoneyService() {
log.info(">>> constructing SendMoneyService! << }

@Override
public boolean sendMoney(SendMoneyCommand command) {
log.info("sending money!");
return false;
}

}
/<code>

想象一下,該類中有一些非常複雜的業務邏輯正在代替日誌記錄語句。

對於本文的大部分內容,我們對SendMoneyUseCase接口的實際實現不感興趣。畢竟,我們想在對Web控制器的測試中模擬它。

為什麼要模擬?

為什麼在測試中我們應該使用模擬而不是真實的服務對象?

想象一下,上面的服務實現依賴於數據庫或某些其他第三方系統。我們不想針對數據庫運行測試,如果數據庫不可用,即使我們的被測系統可能完全沒有錯誤,測試也將失敗。我們在測試中添加的依賴項越多,測試失敗的原因就越多。如果我們改用模擬,則可以模擬掉所有潛在的故障。

除了減少故障之外,模擬還降低了測試的複雜性,從而節省了我們的精力。設置用於測試的正確初始化的對象的整個網絡需要大量樣板代碼。使用模擬,我們只需要“實例化”一個模擬即可,而不是整個對象的真實實例可能需要實例化。

總之,我們希望從可能複雜,緩慢且不穩定的集成測試過渡到簡單,快速和可靠的單元測試

因此,在上述測試中,我們要使用具有相同接口的模擬程序SendMoneyController,而不是的真實實例SendMoneyUseCase,該接口的行為我們可以在測試中根據需要進行控制。

使用Mockito進行模擬(並且沒有Spring)

作為模擬框架,我們將使用Mockito,因為它是全面、完善和集成到Spring Boot中的。

但是最好的測試根本不使用Spring,因此讓我們首先來看一下如何在普通的單元測試中使用Mockito來消除不需要的依賴項。

普通Mockito測試

使用Mockito的最簡單的方法是使用實​​例化一個模擬對象Mockito.mock(),然後將如此創建的模擬對象傳遞給被測類:

<code>public class SendMoneyControllerPlainTest {

private SendMoneyUseCase sendMoneyUseCase =
Mockito.mock(SendMoneyUseCase.class);

private SendMoneyController sendMoneyController =
new SendMoneyController(sendMoneyUseCase);

@Test
void testSuccess() {
// given
SendMoneyCommand command = new SendMoneyCommand(1L, 2L, 500);
given(sendMoneyUseCase
.sendMoney(eq(command)))
.willReturn(true);

// when

ResponseEntity response = sendMoneyController
.sendMoney(1L, 2L, 500);

// then
then(sendMoneyUseCase)
.should()
.sendMoney(eq(command));

assertThat(response.getStatusCode())
.isEqualTo(HttpStatus.OK);
}

}
/<code>

我們創建的模擬實例,SendMoneyService並將該模擬傳遞給的構造函數SendMoneyController。控制器不知道它是模擬的,會像對待真實物體一樣對待它。

在測試本身中,我們可以使用Mockito given()來定義我們希望該模擬具有的行為,並then()檢查是否已按預期調用了某些方法,可以在docs中找到有關Mockito的模擬和驗證方法的更多信息。

Web控制器應該經過集成測試!

不要在家做!上面的代碼只是如何創建模擬的示例。使用這樣的單元測試來測試Spring Web Controller只能覆蓋生產中可能發生的潛在錯誤的一小部分。上面的單元測試驗證是否返回了特定的響應代碼,但是它沒有與Spring集成以檢查是否從HTTP請求中正確解析了輸入參數,或者控制器是否偵聽了正確的路徑,或者是否將異常轉換為預期的HTTP響應,依此類推。

如我在有關@WebMvcTest註解的文章中所討論的,Web控制器應該與Spring集成進行測試。

在JUnit Jupiter中使用Mockito註釋

Mockito提供了一些方便的註釋,減少了創建模擬實例並將它們傳遞到我們要測試的對象中的手動工作。

使用JUnit Jupiter,我們需要將應用於MockitoExtension測試:

<code>@ExtendWith(MockitoExtension.class)
class SendMoneyControllerMockitoAnnotationsJUnitJupiterTest {

@Mock
private SendMoneyUseCase sendMoneyUseCase;

@InjectMocks
private SendMoneyController sendMoneyController;

@Test
void testSuccess() {
...
}

}
/<code>

然後,我們可以在測試字段中使用@Mock和@InjectMocks註釋。

帶註解的字段@Mock將自動使用其類型的模擬實例進行初始化,就像我們Mockito.mock()手動調用一樣。

然後,Mockito將嘗試@InjectMocks通過將所有模擬傳遞到構造函數來實例化帶註釋的字段。請注意,我們需要為Mockito提供這樣的構造函數以使其可靠地工作。如果Mockito找不到構造函數,它將嘗試使用setter注入或字段注入,但是最乾淨的方法仍然是構造函數。您可以在Mockito的Javadoc中閱讀有關其背後的算法的信息。

在JUnit 4中使用Mockito註釋

與JUnit 4相似,除了我們需要使用MockitoJUnitRunner代替MockitoExtension:

<code>@RunWith(MockitoJUnitRunner.class)
public class SendMoneyControllerMockitoAnnotationsJUnit4Test {

@Mock
private SendMoneyUseCase sendMoneyUseCase;

@InjectMocks
private SendMoneyController sendMoneyController;

@Test
public void testSuccess() {
...
}

}
/<code>

使用Mockito和Spring Boot進行模擬

有時候,我們不得不依靠Spring Boot為我們設置應用程序上下文,因為手動實例化整個類的網絡將耗費大量精力。

但是,我們可能不想在某個測試中測試所有Bean之間的集成,因此,我們需要一種在模擬的Spring應用程序上下文中替換某些Bean的方法。Spring Boot 為此提供了@MockBean和@SpyBean註釋。

用@MockBean添加一個模擬Spring Bean

一個使用模擬的主要示例是使用Spring Boot's @WebMvcTest創建一個應用程序上下文,其中包含測試Spring Web控制器所需的所有bean:

<code>@WebMvcTest(controllers = SendMoneyController.class)
class SendMoneyControllerWebMvcMockBeanTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private SendMoneyUseCase sendMoneyUseCase;

@Test
void testSendMoney() {
...
}

}
/<code>

即使由Bean 創建的應用程序上下文被標記為帶有註釋的Spring Bean,@WebMvcTest也不會選擇我們的SendMoneyServiceBean(實現SendMoneyUseCase接口)@Component。我們必須自己提供一個類型的bean SendMoneyUseCase,否則,將得到如下錯誤:

<code>No qualifying bean of type 'io.reflectoring.mocking.SendMoneyUseCase' available:
expected at least 1 bean which qualifies as autowire candidate.
/<code>

無需實例化SendMoneyService自己或告訴Spring進行拾取,而可能在過程中牽扯到其他bean,我們可以SendMoneyUseCase嚮應用程序上下文中添加一個模擬實現。

通過使用Spring Boot的@MockBean註釋可以輕鬆完成此操作。然後,Spring Boot測試支持將自動創建Mockito模擬類型SendMoneyUseCase,並將其添加到應用程序上下文中,以便我們的控制器可以使用它。在測試方法中,我們可以像上面一樣使用Mockito given()和when()方法。

這樣,我們可以輕鬆地創建一個集中化的Web控制器測試,該測試僅實例化所需的對象。

用@MockBean替換Spring Bean

除了添加新的(模擬)bean之外,我們可以@MockBean類似地使用模擬來替換應用程序上下文中已經存在的bean:

<code>@SpringBootTest
@AutoConfigureMockMvc
class SendMoneyControllerSpringBootMockBeanTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private SendMoneyUseCase sendMoneyUseCase;

@Test
void testSendMoney() {
...
}

}
/<code>

請注意,上面的測試使用@SpringBootTest代替@WebMvcTest,這意味著將為此測試創建Spring Boot應用程序的完整應用程序上下文。這包括我們的SendMoneyServicebean,因為它帶有註釋@Component並位於我們的應用程序類的包結構之內。

該@MockBean註釋將導致春季尋找類型的現有的bean SendMoneyUseCase在應用程序上下文。如果存在,它將用Mockito模擬替換該bean。

最終結果是相同的:在我們的測試中,我們可以sendMoneyUseCase像對待Mockito模擬一樣對待對象。

區別在於,在SendMoneyService創建初始應用程序上下文之前,將在使用模擬替換Bean之前實例化Bean。如果SendMoneyService在其構造函數中執行了某些操作,而該操作需要依賴於測試時不可用的數據庫或第三方系統,則此操作將無效。除了使用之外@SpringBootTest,我們還必須創建一個更具針對性的應用程序上下文,並在實例化實際的bean之前將模擬添加到應用程序上下文中。

使用@SpyBean監視Spring Bean

Mockito還允許我們監視真實對象。Mockito不會完全嘲笑一個對象,而是圍繞真實對象創建一個代理,並僅監視正在調用的方法,以便稍後我們可以驗證是否已調用某個方法。

Spring Boot @SpyBean為此提供了註釋:

<code>@SpringBootTest
@AutoConfigureMockMvc
class SendMoneyControllerSpringBootSpyBeanTest {

@Autowired
private MockMvc mockMvc;

@SpyBean
private SendMoneyUseCase sendMoneyUseCase;

@Test
void testSendMoney() {
...
}

}
/<code>

@SpyBean就像@MockBean。與其在應用程序上下文中添加或替換bean,不如將其包裝在Mockito的代理中。在測試中,我們可以then()如上所述使用Mockito 驗證方法調用。

為什麼我的Spring測試需要這麼長時間?

如果我們使用@MockBean,並@SpyBean在我們的測試了很多,運行測試將花費大量的時間。這是因為Spring Boot為每個測試創建了一個新的應用程序上下文,根據應用程序上下文的大小,這可能是一項昂貴的操作。

結論

Mockito使我們可以輕鬆地模擬掉我們現在不想測試的對象。這可以減少我們測試中的集成開銷,甚至可以將集成測試轉變為更具針對性的單元測試。

Spring Boot通過使用@MockBean和@SpyBean註釋,可以輕鬆地在Spring支持的集成測試中使用Mockito的模擬功能。

儘管這些Spring Boot功能要包含在我們的測試中一樣容易,但我們應該意識到成本:每個測試都可能創建一個新的應用程序上下文,從而潛在地增加了我們測試套件的運行時間。


分享到:


相關文章: