轉:http://blog.csdn.net/yys79/article/details/66472797
最近,項目中頻繁用到dubbo,而且java工程用引用了幾十個關聯系統的服務(如用戶認證,基礎服務,客戶平台)。這些服務都是dubbo服務,對我們僅提供了一個接口,服務通過zookeeper注冊,並給我們提供服務。我們的項目都是基於spring的。spring集成dubbo,就可以對這些外部服務進行注入和使用了。
但是對於單元測試來說卻出現了難題:領域模型的測試不是問題,主要都是自己的代碼,加上一些mock就可以輕松測試;但是如果我想測試應用服務層(使用外部服務最多的地方),很多情況下就需要啟動spring環境,而這樣就需要加載外部系統的服務了。問題是外部的服務給我們的jar包中,只有服務的接口。啟動時如果按照正常開發環境的配置加載spring context,那么明顯是依賴了外部環境,如果沒有啟動zookeeper或者本機不聯網,抑或是關聯系統沒有啟動,spring context加載將會失敗,這是單元測試的忌諱。如果使用專門的單元測試的spring配置文件,去掉外部關聯系統的consumer配置,啟動會直接失敗,更別提測試了。
還有寫其他問題,如測試靜態方法,私有方法;mock框架與springtest如何集成。spring的aop代理類如何mock一些默認的實現,測試數據庫如何選擇。總之問題超多。好吧,該進入正題了。
1.測試靜態類,私有方法的問題
簡單一句話,用powermock。powermock可以做到修改字節碼而改變類的行為,這不多說了,大家自己搜一下,官網上例子通俗易懂。目前我在maven中的關於powermock,mockito的依賴是這樣加入的:
- <dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-api-mockito</artifactId>
- <version>1.6.6</version>
- </dependency>
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-all</artifactId>
- <version>1.10.19</version>
- </dependency>
- <dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-module-junit4</artifactId>
- <version>1.6.6</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-module-junit4-rule-agent</artifactId>
- <version>1.6.6</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-module-junit4-rule</artifactId>
- <version>1.6.6</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.jacoco</groupId>
- <artifactId>org.jacoco.agent</artifactId>
- <classifier>runtime</classifier>
- <version>0.7.9</version>
- <scope>test</scope>
- </dependency>
最后的話這個jacoco不是mock的依賴,是一個測試覆蓋率的插件。也推薦一下給大家用,哈哈。
2.powermock與springtest配合使用的問題
第一個問題解決了,不錯!第二個問題就來了。spring標准的Runner是SpringJUnit4ClassRunner,如果用這個Runner,那么powermock的@PrepairForTest就沒法使用了(也就是靜態mock,私有方法mock的關鍵),因此如果想使用靜態和私有方法mock就必須使用用Powemock的Runner,但是又如何啟動spring context呢?
經過一些查找,終於解決了這個問題,方法就是用powermock的代理, 在測試類上加上這樣的注解:
- @PowerMockIgnore({"java.lang.management.*","javax.management.*","javax.xml.*","org.xml.sax.*","org.apache.xerces.*","org.w3c.*"})
- @RunWith(PowerMockRunner.class)
- @PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
- @ContextConfiguration(locations = "classpath:META-INF/spring/test-spring.xml")
Runner使用PowerMockRuner(就是RunWith注解的值);使用powermock提供的代理來使用SpringJUnit4ClassRunner;@PowerMockIgnore的作用是忽略一些powermock使用的classloader無法處理的類,不使用的話,啟動用例就會報錯。
- classpath:META-INF/spring/test-spring.xml 是單元測試專門的spring配置文件,和域代碼使用的配置有些不同。這個文件我放在/test/resources/spring/目錄下。
到此,一個基於PowerMock,springtest和Mockito的基本配置就都弄完了。
上一篇說到powermock的配置,我一般在測試類中再加上繼承spring的測試類:extends AbstractTransactionalJUnit4SpringContextTests ,這樣就基本可以了。
再來說說上一篇中使用的spring配置文件。主要的不同就是test-spring.xml里面不會包含哪些引用外部服務的consumer,也就是剔除外部dubbo服務。
但是代碼里有很多注入外部服務的地方,這如何處理呢?這是第三個問題:
3.注入外部的服務:
開始我想了個很笨的方法:在test/文件夾下給外部服務的接口都提供一個空的實現類(implements 接口,然后用eclpse生成默認的方法實現)。這樣基本上就可以啟動了。但是實際使用中,由於外部服務接口也在不斷修改中,會出現不同環境的接口類不一至的情況。比如uat環境的jar包多了或一個方法(雖然我們的程序沒有直接使用),如此一來,我自己搞的空實現類就會報編譯錯誤了。
后來想到了一個方法,在/test的代碼中增加一個普通的@Conponent注解的類,類里面使用@Bean注解標明所有外部類的生成方法
- @Component
- public class MockedOuterBeanFactory {
- @Bean
- public OuterService outerSerive(){
- return Mocktio.mock(OuterService.class);
- }
- }
然后在測試類中注入這個MockedOuterBeanFactory,這樣測試環境的spring就可以完整的啟動了。外部的服務在啟動后都是Mocktio生成的代理類,所有方法都會返回默認值。
在實際測試中如何打樁呢?也很簡單。
如果我測試一個自己寫的服務(如MyService),MyService又注入了OuterService(外部服務),那么利用spring Bean注入的單例這個特性就可以完成。在MyService的測試類中(MyServiceTest.java),同樣也注入OuterService,在執行MyService的方法之前對OuterService進行打樁。那么由於bean是單例的,MyServiceTest中注入的OuterService實例就是MyService注入的實例。這樣就輕松完成了打樁的工作。如果有特殊原因,main中配置的bean不是單例的,那么可以的話,在test-spring.xml中把它配置為單例的就可以。如果確實情況特殊不允許配置為單例方式,看下一篇吧。
啟動后
解決了spring啟動的問題,然后呢?數據庫
4.測試數據庫的選擇
有時候,我們需要測試持久化的內容,比如分頁查詢,不能說測試覆蓋了代碼就可以,還需要驗證查詢到的數據是否符合要求。參考了dbunits之類的東西,最后還是覺得之前使用的h2database是最好的選擇。它可以使用內存模式,不需要外部數據庫的依賴。這樣單元測試才能獨立運行。配置很簡單,
首先加入依賴:
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <span style="white-space:pre"> </span><version>1.4.191</version>
- </dependency>
至於版本,就自己找個最新的吧。
然后在數據源的地方使用如下配置(這也是測試環境spring配置不同於main配置的主要位置):
- <bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource"
- destroy-method="close">
- <property name="poolProperties">
- <bean class="org.apache.tomcat.jdbc.pool.PoolProperties">
- <property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=1;MODE=MySQL"/>
- <property name="driverClassName" value="org.h2.Driver" />
- <property name="username" value="" />
- <property name="password" value=""/>
- <property name="validationQuery" value="SELECT 1"/>
- <property name="maxActive" value="8" />
- <property name="minIdle" value="1"/>
- <property name="maxIdle" value="4" />
- <property name="maxWait" value="10000"/>
- <property name="initialSize" value="1"/>
- </bean>
- </property>
- </bean>
注意:MODE=MySQL,這是讓h2模擬mysql庫,如果你使用其他類型的庫,一般也會有對應的Mode,主流數據庫都支持。注意mem項,意思是內存數據庫,這樣配置根本不會生成數據庫文件的,特別適合單元測試(依賴外部環境就不是標准單元測試了)。至於數據源類型,按自己的工程的配置就好,只要使用h2的url和driver就行,這里用的是tomcat數據源。
這些配置都做好后,就可以運行真正的powermock,mockito,springtest的單元測試了。下一篇說說怎么測試aop的類。
上兩篇中,基本環境和測試方式都說了一下。基本的測試否沒問題了。但是還有些問題需要解決。在我實際的開發中,最主要是是要做有Aop切面的Bean內部注入的bean打樁。
基本情況是:
MyService是個接口,其實現類MyServiceImpl是@Transactional注解的Bean(這樣注入的MyService實例實際上就是代理了)
MyServiceImpl注了一個Bean:InnerBean,innerBean是自己工程中實現或其他服務都無所謂
測試中想使用mock替換這個InnerBean。
在spring中,aop用代理實現的。PowerMock不能修改其字節碼。而在測試中,我需要替換MyService代理中的InnerBean實例。開始傷透了腦筋啊。。。
如果不能打樁,那么必須老老實實的准備fixture才能測試,比如准備數據庫中多個表的數據,才能保證InnerBean完成我的預期結果(這種情況還算好的,有些情況都不能打樁)。
這個其實真是不難,只不過之前不太熟悉spring的測試框架(以前拋棄了spring,所以也不怎么研究)。
springtest有2個Utils類,可以幫助我們拿到MyService代理中的具體實現類:
- org.springframework.test.util.AopTestUtils;
- org.springframework.test.util.ReflectionTestUtils;
- MyServiceImpl impl = org.springframework.test.util.AopTestUtils.getTargetObject(MyServiceBean實例);
這樣就可以拿到具體實現類了,再加一句impl.innerBean = mockInnerBean;就可以用自己打樁過的mock替換注入的innerBean實例了。如果多於一個測試方法,別忘了finally時候替換回來啊。
impl.innerBean 這里,我一般的注入bean都是是用package級別的,這樣便於測試,不必特別的依賴其他技術就可以替換實現。如果是private的,那么用ReflectionTestUtils吧,具體不用說了,簡單易用。