javascript
使用Mockito和BeanPostProcessors在Spring注入测试双打
我非常確定,如果您曾經使用過Spring并且熟悉單元測試,那么您會遇到與您不想修改的Spring應用程序上下文中注入模擬/間諜(測試雙打)有關的問題。 本文介紹了一種使用Spring組件解決此問題的方法。
項目結構
讓我們從項目結構開始:
像往常一樣提出問題,我試圖顯示一個非常簡單的項目結構。 如果我像我們在項目中那樣擴大問題的范圍,我將要展示的方法可能會顯示出更多的好處:
- 我們有數十個接口和實現自動連接到列表
- 我們希望基于現有的Spring應用程序上下文執行一些功能測試
- 我們想要驗證對于某些輸入條件,某些特定的實現將執行其方法
- 我們想存根數據庫訪問。
在這個例子中,我們有一個PlayerService ,它使用PlayerWebService獲取一個Player 。 我們有一個applicationContext,它僅定義用于自動裝配的包:
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"><context:component-scan base-package="com.blogspot.toomuchcoding"/></beans>然后我們有一個非常簡單的模型:
播放器
package com.blogspot.toomuchcoding.model;import java.math.BigDecimal;/*** User: mgrzejszczak* Date: 08.08.13* Time: 14:38*/ public final class Player {private final String playerName;private final BigDecimal playerValue;public Player(final String playerName, final BigDecimal playerValue) {this.playerName = playerName;this.playerValue = playerValue;}public String getPlayerName() {return playerName;}public BigDecimal getPlayerValue() {return playerValue;} }PlayerService的實現,該實現使用PlayerWebService檢索有關Player數據:
PlayerServiceImpl.java
package com.blogspot.toomuchcoding.service;import com.blogspot.toomuchcoding.model.Player; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;/*** User: mgrzejszczak* Date: 08.06.13* Time: 19:02*/ @Service public class PlayerServiceImpl implements PlayerService {private static final Logger LOGGER = LoggerFactory.getLogger(PlayerServiceImpl.class);@Autowiredprivate PlayerWebService playerWebService;@Overridepublic Player getPlayerByName(String playerName) {LOGGER.debug(String.format("Logging the player web service name [%s]", playerWebService.getWebServiceName()));return playerWebService.getPlayerByName(playerName);}public PlayerWebService getPlayerWebService() {return playerWebService;}public void setPlayerWebService(PlayerWebService playerWebService) {this.playerWebService = playerWebService;} }作為數據提供者的PlayerWebService的實現(在這種情況下,我們正在模擬等待響應的時間):
PlayerWebServiceImpl.java
package com.blogspot.toomuchcoding.service;import com.blogspot.toomuchcoding.model.Player; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service;import java.math.BigDecimal;/*** User: mgrzejszczak* Date: 08.08.13* Time: 14:48*/ @Service public class PlayerWebServiceImpl implements PlayerWebService {private static final Logger LOGGER = LoggerFactory.getLogger(PlayerWebServiceImpl.class);public static final String WEB_SERVICE_NAME = "SuperPlayerWebService";public static final String SAMPLE_PLAYER_VALUE = "1000";@Overridepublic String getWebServiceName() {return WEB_SERVICE_NAME;}@Overridepublic Player getPlayerByName(String name) {try {LOGGER.debug("Simulating awaiting time for a response from a web service");Thread.sleep(5000);} catch (InterruptedException e) {LOGGER.error(String.format("[%s] occurred while trying to make the thread sleep", e));}return new Player(name, new BigDecimal(SAMPLE_PLAYER_VALUE));} }也許項目的結構和方法不是您見過的最出色的方法之一,但我想讓問題的表達保持簡單;)
問題
那么到底是什么問題呢? 讓我們假設我們希望自動連接的PlayerWebServiceImpl是可以驗證的間諜。 而且,您不想實際更改applicationContext.xml任何內容,而是想要使用Spring上下文的當前版本。
使用模擬程序更容易,因為您可以在XML文件中定義(使用Mockito工廠方法),將bean作為模擬程序來覆蓋原始實現,如下所示:
<bean id="playerWebServiceImpl" class="org.mockito.Mockito" factory-method="mock"><constructor-arg value="com.blogspot.toomuchcoding.service.PlayerWebServiceImpl"/></bean>那間諜呢? 因為要創建間諜,您需要給定類型的現有對象,因此問題更加嚴重。 在我們的示例中,我們進行了一些自動裝配,因此我們必須首先創建一個PlayerWebService類型的spring bean(Spring必須連接其所有依賴項),然后將其包裝在Mockito.spy(...) ,然后是否必須將其連接到其他地方…變得非常復雜,不是嗎?
解決方案
您可以看到問題并不是那么容易解決的。 解決該問題的一種簡單方法是使用本機Spring機制– BeanPostProcessors。 您可以查看有關如何為指定類型創建Spring BeanPostProcessor的文章-在本示例中將使用它。
讓我們從檢查測試類開始:
PlayerServiceImplTest.java
package com.blogspot.toomuchcoding.service;import com.blogspot.toomuchcoding.model.Player; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import java.math.BigDecimal;import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.doReturn; import static org.mockito.Mockito.verify;/*** User: mgrzejszczak* Date: 08.06.13* Time: 19:26*/ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:testApplicationContext.xml") public class PlayerServiceImplTest {public static final String PLAYER_NAME = "Lewandowski";public static final BigDecimal PLAYER_VALUE = new BigDecimal("35000000");@AutowiredPlayerWebService playerWebServiceSpy;@AutowiredPlayerService objectUnderTest;@Testpublic void shouldReturnAPlayerFromPlayerWebService(){//givenPlayer referencePlayer = new Player(PLAYER_NAME, PLAYER_VALUE);doReturn(referencePlayer).when(playerWebServiceSpy).getPlayerByName(PLAYER_NAME);//whenPlayer player = objectUnderTest.getPlayerByName(PLAYER_NAME);//thenassertThat(player, is(referencePlayer));verify(playerWebServiceSpy).getWebServiceName();assertThat(playerWebServiceSpy.getWebServiceName(), is(PlayerWebServiceImpl.WEB_SERVICE_NAME));}}在此測試中,我們希望模擬從PlayerWebService檢索Player (假設正常情況下它將嘗試向外界發送請求,并且我們不希望這種情況發生),并測試PlayerService返回了我們在方法存根中提供的Player ,以及我們想對Spy進行驗證,以確認方法getWebServiceName()已執行并且其返回值定義得非常精確。 換句話說,我們想對方法getPlayerByName(...)進行存根,并希望通過檢查getWebServiceName()方法來對間諜進行驗證。
讓我們檢查一下測試上下文:
testApplicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><import resource="applicationContext.xml"/><bean class="com.blogspot.postprocessor.PlayerWebServicePostProcessor" /> </beans>測試上下文非常小,因為它會導入當前的applicationContext.xml并創建一個Bean,這是此示例中的關鍵功能– BeanPostProcessor :
PlayerWebServicePostProcessor.java
package com.blogspot.postprocessor;import com.blogspot.toomuchcoding.processor.AbstractBeanPostProcessor; import com.blogspot.toomuchcoding.service.PlayerWebService;import static org.mockito.Mockito.spy;/*** User: mgrzejszczak* Date: 07.05.13* Time: 11:30*/ public class PlayerWebServicePostProcessor extends AbstractBeanPostProcessor<PlayerWebService> {public PlayerWebServicePostProcessor() {super(PlayerWebService.class);}@Overridepublic PlayerWebService doBefore(PlayerWebService bean) {return spy(bean);}@Overridepublic PlayerWebService doAfter(PlayerWebService bean) {return bean;} } 該類擴展了實現BeanPostProcessor接口的AbstractBeanPostProcessor 。 這個類背后的邏輯是注冊類為其中一個想要之前任一初始化(執行某些動作postProcessBeforeInitialization )或豆(初始化之后postProcessAfterInitialization )。 我的帖子中很好地解釋了AbstractBeanPostProcessor
Spring BeanPostProcessor用于指定的類型,但是有一點點變化–在我的舊文章中,抽象允許我們對Bean執行一些操作,而不能在Bean上返回包裝器或代理。
如您在初始化之前使用Mockito.spy(...) PlayerWebServicePostProcessor ,我們正在使用Mockito.spy(...)方法創建一個Spy。 通過這種方式,我們在給定類型的Bean的初始化上創建了一個工廠鉤子-就這么簡單。 對于實現PlayerWebService接口的所有類,將執行此方法。
其他可能性
在檢查該問題的當前解決方案時,我遇到了Jakub Janczak的Springockito庫 。
我還沒有使用過它,所以我不知道與此庫相關的生產問題(如果有的話;)),但看起來真的很直觀,很好– Jakub! 盡管如此,您仍然依賴于外部庫,而在此示例中,我展示了如何使用Spring處理問題。
摘要
在這篇文章中,我展示了如何
- 使用XML Spring配置為現有bean創建模擬
- 創建一個BeanPostProcessor實現,該實現對給定類的bean執行邏輯
- 對于給定的bean類,返回Spy(您也可以返回Mock)
現在,讓我們看一下我的方法的優點和缺點:
優點
- 您使用Spring本機機制為您的bean創建測試雙打
- 您不需要添加任何其他外部依賴項
- 如果您使用AbstractBeanPostProcessor ,則只需執行很少的更改
缺點
- 您必須熟悉內部Spring體系結構(它使用BeanPostProcessors)–但這是不利嗎? ;)–實際上,如果您使用AbstractBeanPostProcessor ,則不必熟悉它–您只需提供類類型和初始化前后要發生的操作即可。
- 它不像Springockito庫中的注釋那樣直觀
資料來源
源代碼可從TooMuchCoding BitBucket存儲庫和TooMuchCoding Github存儲庫中獲得 。
參考:在博客上 使用我們的JCG合作伙伴 Marcin Grzejszczak的Mockito和BeanPostProcessors在Spring注入測試雙打, 用于編碼成癮者博客。翻譯自: https://www.javacodegeeks.com/2013/08/injecting-test-doubles-in-spring-using-mockito-and-beanpostprocessors.html
總結
以上是生活随笔為你收集整理的使用Mockito和BeanPostProcessors在Spring注入测试双打的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 漂流少女设置(漂流少女活动)
- 下一篇: 使用ActiveMQ和HornetQ通过