javascript
如何模拟Spring bean(版本2)
大約一年前,我寫了一篇博客文章如何模擬Spring Bean 。 所描述的模式對生產代碼幾乎沒有侵入性。 正如讀者Colin在評論中正確指出的那樣,基于@Profile注釋的間諜/模擬Spring bean是更好的選擇。 這篇博客文章將描述這種技術。 我在工作中以及副項目中都成功使用了這種方法。
請注意,在您的應用程序中普遍出現的嘲笑通常被視為設計氣味。
介紹生產代碼
首先,我們需要測試代碼以演示模擬。 我們將使用以下簡單的類:
@Repository public class AddressDao {public String readAddress(String userName) {return "3 Dark Corner";} }@Service public class AddressService {private AddressDao addressDao;@Autowiredpublic AddressService(AddressDao addressDao) {this.addressDao = addressDao;}public String getAddressForUser(String userName){return addressDao.readAddress(userName);} }@Service public class UserService {private AddressService addressService;@Autowiredpublic UserService(AddressService addressService) {this.addressService = addressService;}public String getUserDetails(String userName){String address = addressService.getAddressForUser(userName);return String.format("User %s, %s", userName, address);} }當然,這段代碼沒有多大意義,但對于演示如何模擬Spring bean來說將是一個很好的選擇。 AddressDao只是返回字符串,因此模擬了從某些數據源的讀取。 它自動連接到AddressService 。 該bean被自動連接到UserService ,后者用于構造帶有用戶名和地址的字符串。
請注意,我們將構造函數注入用作字段注入被認為是不好的做法。 如果要為應用程序強制執行構造函數注入,Oliver Gierke(Spring生態系統開發人員和Spring Data負責人)最近創建了一個非常不錯的項目Ninjector 。
掃描所有這些bean的配置是相當標準的Spring Boot主類:
@SpringBootApplication public class SimpleApplication {public static void main(String[] args) {SpringApplication.run(SimpleApplication.class, args);} }模擬Spring Bean(無AOP)
讓我們在模擬AddressDao地方測試AddressService類。 我們可以創建通過Spring”這個模擬@Profiles和@Primary注釋是這樣的:
@Profile("AddressService-test") @Configuration public class AddressDaoTestConfiguration {@Bean@Primarypublic AddressDao addressDao() {return Mockito.mock(AddressDao.class);} }僅當Spring概要文件AddressService-test處于活動狀態時,才會應用此測試配置。 應用時,它將注冊AddressDao類型的bean,該類型是Mockito創建的模擬實例。 @Primary注釋告訴Spring在有人自動裝配AddressDao bean時使用此實例,而不是實際實例。
測試類使用的是JUnit框架:
@ActiveProfiles("AddressService-test") @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(SimpleApplication.class) public class AddressServiceITest {@Autowired private AddressService addressService;@Autowiredprivate AddressDao addressDao;@Testpublic void testGetAddressForUser() {// GIVENMockito.when(addressDao.readAddress("john")).thenReturn("5 Bright Corner");// WHEN String actualAddress = addressService.getAddressForUser("john");// THEN Assert.assertEquals("5 Bright Corner", actualAddress);} }我們激活配置文件AddressService-test以啟用AddressDao AddressService-test 。 Spring集成測試需要使用@RunWith注釋,而@SpringApplicationConfiguration定義將使用哪種Spring配置來構造測試環境。 在測試之前,我們自動連接被測試的AddressService實例和AddressDao模擬。
如果您使用的是Mockito,則隨后的測試方法應明確。 在GIVEN階段,我們將所需的行為記錄到模擬實例中。 在WHEN階段,我們執行測試代碼,在THEN階段,我們驗證測試代碼是否返回了我們期望的值。
監視Spring Bean(無AOP)
對于間諜示例,將在AddressService實例上進行間諜:
@Profile("UserService-test") @Configuration public class AddressServiceTestConfiguration {@Bean@Primarypublic AddressService addressServiceSpy(AddressService addressService) {return Mockito.spy(addressService);} }僅當配置文件UserService-test處于活動狀態時,才會對此組件配置進行Spring掃描。 它定義了AddressService類型的主bean。 @Primary告訴Spring使用該實例,以防在Spring上下文中存在兩個這種類型的bean。 在構造此bean的過程中,我們從Spring上下文自動裝配了AddressService現有實例,并使用Mockito的間諜功能。 我們正在注冊的bean有效地將所有調用委托給原始實例,但是Mockito間諜程序使我們可以驗證所偵查實例的交互。
我們將以這種方式測試UserService行為:
@ActiveProfiles("UserService-test") @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(SimpleApplication.class) public class UserServiceITest {@Autowiredprivate UserService userService;@Autowiredprivate AddressService addressService;@Testpublic void testGetUserDetails() {// GIVEN - Spring scanned by SimpleApplication class// WHENString actualUserDetails = userService.getUserDetails("john");// THENAssert.assertEquals("User john, 3 Dark Corner", actualUserDetails);Mockito.verify(addressService).getAddressForUser("john");} }為了進行測試,我們激活了UserService-test配置文件,因此將應用我們的間諜配置。 我們自動裝配UserService這是在測試和AddressService ,目前正在通過窺探的Mockito。
我們不需要為在GIVEN階段進行測試準備任何行為。 W HEN相被測明顯執行代碼。 在THEN階段,我們驗證測試代碼是否返回了我們期望的值,以及是否使用正確的參數執行了addressService調用。
Mockito和Spring AOP的問題
假設現在我們要使用Spring AOP模塊來處理一些跨領域的問題。 例如,以這種方式記錄對Spring Bean的調用:
package net.lkrnac.blog.testing.mockbeanv2.aoptesting;import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component;import lombok.extern.slf4j.Slf4j;@Aspect @Component @Slf4j @Profile("aop") //only for example purposes public class AddressLogger {@Before("execution(* net.lkrnac.blog.testing.mockbeanv2.beans.*.*(..))")public void logAddressCall(JoinPoint jp){log.info("Executing method {}", jp.getSignature());} }在從包net.lkrnac.blog.testing.mockbeanv2調用Spring bean之前,將應用此AOP方面。 它使用Lombok的注釋@Slf4j記錄調用方法的簽名。 注意,僅當定義了aop概要文件時才創建此bean。 我們正在使用此配置文件將AOP和非AOP測試示例分開。 在實際的應用程序中,您不想使用此類配置文件。
我們還需要為我們的應用程序啟用AspectJ,因此以下所有示例都將使用此Spring Boot主類:
@SpringBootApplication @EnableAspectJAutoProxy public class AopApplication {public static void main(String[] args) {SpringApplication.run(AopApplication.class, args);} }AOP構造由@EnableAspectJAutoProxy啟用。
但是,如果我們將Mockito與Spring AOP結合進行模擬,則此類AOP構造可能會出現問題。 這是因為兩者都使用CGLIB代理真實實例,并且當Mockito代理包裝到Spring代理中時,我們會遇到類型不匹配的問題。 這些可以通過使用ScopedProxyMode.TARGET_CLASS配置bean的作用域來ScopedProxyMode.TARGET_CLASS ,但是Mockito的verify ()調用仍然會因NotAMockException失敗。 如果我們為UserServiceITest啟用aop配置文件,則可以看到此類問題。
由Spring AOP代理的模擬Spring Bean
為了克服這些問題,我們將模擬包裝到這個Spring bean中:
package net.lkrnac.blog.testing.mockbeanv2.aoptesting;import org.mockito.Mockito; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository;import lombok.Getter; import net.lkrnac.blog.testing.mockbeanv2.beans.AddressDao;@Primary @Repository @Profile("AddressService-aop-mock-test") public class AddressDaoMock extends AddressDao{@Getterprivate AddressDao mockDelegate = Mockito.mock(AddressDao.class);public String readAddress(String userName) {return mockDelegate.readAddress(userName);} }@Primary注釋可確保在注入過程中,此bean優先于實際的AddressDao bean。 為了確保僅將其應用于特定測試,我們為此bean定義了配置文件AddressService-aop-mock-test 。 它繼承了AddressDao類,因此可以完全替代該類型。
為了偽造行為,我們定義了AddressDao類型的模擬實例,該實例通過由Lombok的@Getter批注定義的getter @Getter 。 我們還實現了readAddress()方法,該方法有望在測試期間被調用。 此方法僅將調用委派給模擬實例。
使用該模擬程序的測試如下所示:
@ActiveProfiles({"AddressService-aop-mock-test", "aop"}) @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(AopApplication.class) public class AddressServiceAopMockITest {@Autowiredprivate AddressService addressService; @Autowiredprivate AddressDao addressDao;@Testpublic void testGetAddressForUser() {// GIVENAddressDaoMock addressDaoMock = (AddressDaoMock) addressDao;Mockito.when(addressDaoMock.getMockDelegate().readAddress("john")).thenReturn("5 Bright Corner");// WHEN String actualAddress = addressService.getAddressForUser("john");// THEN ?Assert.assertEquals("5 Bright Corner", actualAddress);} }在測試中,我們定義AddressService-aop-mock-test配置文件以激活AddressDaoMock并定義aop配置文件以激活AddressLogger AOP方面。 為了進行測試,我們自動裝配了bean addressService及其偽造的依賴項addressDao 。 我們知道, addressDao將是AddressDaoMock類型的,因為此bean被標記為@Primary 。 因此,我們可以將其mockDelegate轉換mockDelegate行為記錄到mockDelegate 。
當我們調用測試方法時,應使用記錄的行為,因為我們希望測試方法使用AddressDao依賴項。
監視Spring AOP代理的Spring bean
類似的模式可用于監視實際實現。 這就是我們的間諜的樣子:
package net.lkrnac.blog.testing.mockbeanv2.aoptesting;import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service;import lombok.Getter; import net.lkrnac.blog.testing.mockbeanv2.beans.AddressDao; import net.lkrnac.blog.testing.mockbeanv2.beans.AddressService;@Primary @Service @Profile("UserService-aop-test") public class AddressServiceSpy extends AddressService{@Getterprivate AddressService spyDelegate;@Autowiredpublic AddressServiceSpy(AddressDao addressDao) {super(null);spyDelegate = Mockito.spy(new AddressService(addressDao));}public String getAddressForUser(String userName){return spyDelegate.getAddressForUser(userName);} }如我們所見,該間諜與AddressDaoMock非常相似。 但是在這種情況下,真正的bean使用構造函數注入來自動裝配其依賴關系。 因此,我們需要定義非默認構造函數,并且還要進行構造函數注入。 但是我們不會將注入的依賴項傳遞給父構造函數。
為了啟用對真實對象的監視,我們將構造具有所有依賴項的新實例,將其包裝到Mockito間諜實例中,并將其存儲到spyDelegate屬性中。 我們期望在測試期間調用方法getAddressForUser() ,因此我們將此調用委托給spyDelegate 。 可以在測試中通過由Lombok的@Getter批注定義的getter訪問此屬性。
測試本身如下所示:
@ActiveProfiles({"UserService-aop-test", "aop"}) @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(AopApplication.class) public class UserServiceAopITest {@Autowiredprivate UserService userService;@Autowiredprivate AddressService addressService;@Testpublic void testGetUserDetails() {// GIVENAddressServiceSpy addressServiceSpy = (AddressServiceSpy) addressService;// WHENString actualUserDetails = userService.getUserDetails("john");// THEN Assert.assertEquals("User john, 3 Dark Corner", actualUserDetails);Mockito.verify(addressServiceSpy.getSpyDelegate()).getAddressForUser("john");} }這是非常簡單的。 配置文件UserService-aop-test確保可以掃描AddressServiceSpy 。 配置文件aop在AddressLogger方面確保相同。 當我們自動測試對象UserService及其依賴項AddressService ,我們知道可以將其spyDelegate為AddressServiceSpy并在調用測試方法后驗證其spyDelegate屬性的調用。
由Spring AOP代理的假Spring Bean
顯然,將調用委派給Mockito模擬或間諜會使測試復雜化。 如果我們僅需要偽造邏輯,那么這些模式通常會被大刀闊斧。 在這種情況下,我們可以使用這些偽造品:
@Primary @Repository @Profile("AddressService-aop-fake-test") public class AddressDaoFake extends AddressDao{public String readAddress(String userName) {return userName + "'s address";} }并將其用于這種方式的測試:
@ActiveProfiles({"AddressService-aop-fake-test", "aop"}) @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(AopApplication.class) public class AddressServiceAopFakeITest {@Autowiredprivate AddressService addressService; @Testpublic void testGetAddressForUser() {// GIVEN - Spring context// WHEN String actualAddress = addressService.getAddressForUser("john");// THEN ?Assert.assertEquals("john's address", actualAddress);} }我認為這個測試不需要解釋。
- 這些示例的源代碼托管在Github上。
翻譯自: https://www.javacodegeeks.com/2016/01/mock-spring-bean-version-2.html
總結
以上是生活随笔為你收集整理的如何模拟Spring bean(版本2)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java超出gc开销限制_超出了GC开销
- 下一篇: gradle idea java ssm