在 Spock Spring 測試中將 Mock 作為 Spring Bean 注入
一、簡介
當我們使用 Spock 測試 Spring 應用程式時,我們有時會想要更改 Spring 管理的元件的行為。在本教程中,我們將學習如何注入我們自己的Stub 、 Mock,或Spy來取代 Spring 自動連接的依賴項。對於大多數範例,我們將使用 Spock Stub ,但當我們使用Mock或Spy.
2. 設定
讓我們先新增依賴項並建立一個具有可替換依賴項的類別。
2.1.依賴關係
首先,我們為 Spring Boot 3 新增Maven 編譯相依性:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
現在,讓我們為spring-boot-starter-test和[spock-spring](https://mvnrepository.com/artifact/org.spockframework/spock-spring) .由於我們使用的是 Spring Boot 3 / Spring 6,因此我們需要 Spock v2.4-M1 或更高版本來取得 Spock 相容的 Spring 註解,因此我們使用2.4-M4-groovy-4.0:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>2.4-M4-groovy-4.0</version>
<scope>test</scope>
</dependency>
2.2.我們的主題
有了我們的依賴項,讓我們建立一個帶有 Spring 管理的DataProvider相依性的AccountService類,我們可以將其用於測試。
首先,讓我們建立我們的AccountService :
@Service
public class AccountService {
private final DataProvider provider;
public AccountService(DataProvider provider) {
this.provider = provider;
}
public String getData(String param) {
return "Fetched: " + provider.fetchData(param);
}
}
現在,讓我們建立一個稍後替換的DataProvider :
@Component
public class DataProvider {
public String fetchData(final String input) {
return "data for " + input;
}
}
3.基礎測試類
現在,讓我們建立一個基本的測試類,使用其常用元件來驗證我們的AccountService 。
我們將使用 Spring 的@ContextConfiguration將AccountService和DataProvider納入範圍並在兩個類別中自動組裝:
@ContextConfiguration(classes = [AccountService, DataProvider])
class AccountServiceTest extends Specification {
@Autowired
DataProvider dataProvider
@Autowired
@Subject
AccountService accountService
def "given a real data provider when we use the real bean then we get our usual response"() {
when: "we fetch our data"
def result = accountService.getData("Something")
then: "our real dataProvider responds"
result == "Fetched: data for Something"
}
}
4.使用Spock的Spring註解
現在我們已經有了基本的測試,讓我們探索存根我們的主題依賴項的選項。
4.1. @StubBeans註解
我們經常編寫測試,希望消除依賴項,並且不關心自訂其回應。我們可以使用 Spock 的@StubBeans註解為類別中的每個依賴項創建Stub 。
因此,讓我們建立一個用 Spock 的@StubBeans註解來註解的測試Specification來存根我們的DataProvider類別:
@StubBeans(DataProvider)
@ContextConfiguration(classes = [AccountService, DataProvider])
class AccountServiceStubBeansTest extends Specification {
@Autowired
@Subject
AccountService accountService // ...
}
請注意,我們不需要為DataProvider宣告一個單獨的Stub ,因為StubBeans註解為我們建立了一個。
當我們的AccountService的getData方法呼叫其fetchData方法時,我們產生的Stub將傳回一個空字串。讓我們建立一個測試來斷言:
def "given a Service with a dependency when we use a @StubBeans annotation then a stub is created and injected to the service"() {
when: "we fetch our data"
def result = accountService.getData("Something")
then: "our StubBeans gave us an empty string response from our DataProvider dependency"
result == "Fetched: "
}
我們產生的DataProvider存根從fetchData傳回一個空String ,導致我們的AccountService的getData回傳“Fetched:”,沒有附加任何內容。
4.2. @SpringBean註解
當我們需要自訂回應時,我們會建立一個Stub 、 Mock,或Spy 。因此,讓我們在測試中使用 Spock 的SpringBean註解(而不是StubBeans註解),將DataProvider替換為 Spock **Stub** :
@SpringBean
DataProvider mockProvider = Stub()
請注意,我們不能將SpringBean宣告為def或Object ,我們需要宣告特定類型,例如DataProvider 。
現在,讓我們建立一個AccountServiceSpringBeanTest Specification ,其中包含一個測試,該測試將存根設定為在呼叫其fetchData方法時傳回「42」:
@ContextConfiguration(classes = [AccountService, DataProvider])
class AccountServiceSpringBeanTest extends Specification { // ...
def "given a Service with a dependency when we use a @SpringBean annotation then our stub is injected to the service"() {
given: "a stubbed response"
mockProvider.fetchData(_ as String) >> "42"
when: "we fetch our data"
def result = accountService.getData("Something")
then: "our SpringBean overrode the original dependency"
result == "Fetched: 42"
}
}
@SpringBean註解確保我們的Stub被注入到AccountService中,以便我們得到存根的「42」回應。即使上下文中存在真正的DataProvider ,我們的@SpringBean-註解Stub也會獲勝。
4.3. @SpringSpy註解
有時,我們需要一個Spy來存取真實物件並修改它的一些回應。因此,讓我們使用 Spock 的SpringSpy註解來用DataProvider **Spy** :
@SpringSpy
DataProvider mockProvider
首先,讓我們建立一個測試來驗證我們的間諜物件的fetchData方法是否被呼叫並傳回真正的「data for Something」回應:
@ContextConfiguration(classes = [AccountService, DataProvider])
class AccountServiceSpringSpyTest extends Specification {
@SpringSpy
DataProvider dataProvider
@Autowired
@Subject
AccountService accountService
def "given a Service with a dependency when we use @SpringSpy and override a method then the original result is returned"() {
when: "we fetch our data"
def result = accountService.getData("Something")
then: "our SpringSpy was invoked once and allowed the real method to return the result"
1 * dataProvider.fetchData(_)
result == "Fetched: data for Something"
}
}
@SpringSpy註解將Spy包裹在自動連接的DataProvider周圍,並確保我們的Spy被注入到AccountService.我們的Spy驗證了DataProvider's fetchData方法已被調用,但未更改其結果。
現在讓我們新增一個測試,其中我們的Spy用「spied」覆蓋結果:
def "given a Service with a dependency when we use @SpringSpy and override a method then our spy's result is returned"() {
when: "we fetch our data"
def result = accountService.getData("Something")
then: "our SpringSpy was invoked once and overrode the original method"
1 * dataProvider.fetchData(_) >> "spied"
result == "Fetched: spied"
}
這次,我們注入的Spy bean 驗證了DataProvider的fetchData方法已被調用,並將其回應替換為「spied」。
4.4. @SpringBoot測試中的@SpringBean
現在我們已經在ContextConfiguration測試中看到了SpringBean註釋,讓我們建立另一個測試類別但使用SpringBootTest:
@SpringBootTest
class AccountServiceSpringBootTest extends Specification {
// ...
}
我們的新類別中的測試與我們在AccountServiceSpringBeanTest,因此我們在此不再重複!
但是,我們的SpringBootTest測試類別只有具有SpringBootApplication才會運行,所以讓我們建立一個TestApplication類別:
@SpringBootApplication
class TestApplication {
static void main(String[] args) {
SpringApplication.run(TestApplication, args)
}
}
當我們執行測試時,Spring Boot 嘗試初始化DataSource 。在我們的例子中,由於我們只使用spring-boot-starter我們沒有DataSource ,所以我們的測試無法初始化:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
因此,讓我們透過在SpringBootTest註解中新增spring.autoconfigure.exclude屬性來排除DataSource自動配置:
@SpringBootTest(properties = ["spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration"])
現在,當我們運行測試時,它運行成功了!
4.4.上下文快取
使用@SpringBean註解時,我們應該注意它對Spring測試框架上下文快取的影響。通常,當我們執行使用@SpringBootTest註解的多個測試時,我們的 Spring 上下文會被快取而不是每次都創建。這使得我們的測試運行得更快。
然而,Spock 的@SpringBean將模擬附加到特定的測試實例,類似於spring-boot-test模組中的@MockBean註解。這可以防止上下文緩存,當我們有大量使用它們的測試時,這會減慢我們的整體測試執行速度。
5. 結論
在本教程中,我們學習如何使用 Spock 的@StubBeans註解來存根 Spring 管理的依賴項。接下來,我們學習如何使用 Spock 的@SpringBean或@SpringSpy註解將依賴項替換為Stub, Mock, or Spy 。最後,我們注意到,過度使用@SpringBean註解會幹擾 Spring 測試框架的上下文緩存,從而減慢測試的整體執行速度。所以,我們在使用這個功能時應該要謹慎!
與往常一樣,本文的程式碼可在 GitHub 上取得。