單元測(cè)試
單測(cè)定義
單元測(cè)試(Unit Testing)又稱為模塊測(cè)試, 是針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來(lái)進(jìn)行正確性檢驗(yàn)的測(cè)試工作,程序模塊在面向?qū)ο缶幊讨幸话闶侵阜椒ā?/p>
單元測(cè)試一般由開(kāi)發(fā)人員來(lái)編寫(xiě),用來(lái)保證程序及功能的正確性。
為什么需要單測(cè)
編寫(xiě)單元測(cè)試代碼并不是一件容易的事情抒和,那為什么還需要去話費(fèi)時(shí)間和精力來(lái)編寫(xiě)單元測(cè)試呢巾兆?
- 減少Bug:如今的項(xiàng)目大多都是多人分模塊協(xié)同開(kāi)發(fā)管怠,當(dāng)各個(gè)模塊集成時(shí)再去發(fā)現(xiàn)問(wèn)題袭祟,定位和溝通成本是非常高的,通過(guò)單元測(cè)試來(lái)保證各個(gè)模塊的正確性捂贿,可以盡早的發(fā)現(xiàn)問(wèn)題纠修,而不時(shí)等到集成時(shí)再發(fā)現(xiàn)問(wèn)題。
- 放心重構(gòu):如今持續(xù)型的項(xiàng)目越來(lái)越多厂僧,代碼不斷的在變化和重構(gòu)扣草,通過(guò)單元測(cè)試,開(kāi)發(fā)可以放心的修改重構(gòu)代碼颜屠,減少改代碼時(shí)心理負(fù)擔(dān)辰妙,提高重構(gòu)的成功率。
- 改進(jìn)設(shè)計(jì):越是良好設(shè)計(jì)的代碼汽纤,一般越容易編寫(xiě)單元測(cè)試上岗,多個(gè)小的方法的單測(cè)一般比大方法(成百上千行代碼)的單測(cè)代碼要簡(jiǎn)單、要穩(wěn)定蕴坪,一個(gè)依賴接口的類一般比依賴具體實(shí)現(xiàn)的類容易測(cè)試肴掷,所以在編寫(xiě)單測(cè)的過(guò)程中敬锐,如果發(fā)現(xiàn)單測(cè)代碼非常難寫(xiě),一般表明被測(cè)試的代碼包含了太多的依賴或職責(zé)呆瞻,需要反思代碼的合理性台夺,進(jìn)而推進(jìn)代碼設(shè)計(jì)的優(yōu)化,形成正向循環(huán)痴脾。
就個(gè)人而言颤介,感受最深的就是,有了單測(cè)后重構(gòu)代碼起來(lái)心里壓力小多了赞赖,其次是通過(guò)單測(cè)減少了很多低級(jí)錯(cuò)誤滚朵。
單測(cè)帶來(lái)的一些問(wèn)題
單測(cè)在解決了一些問(wèn)題的同時(shí)也容易產(chǎn)生一些問(wèn)題
- 學(xué)習(xí)成本:?jiǎn)螠y(cè)框架的學(xué)習(xí)需要一定的成本
- 開(kāi)發(fā)成本:項(xiàng)目初期,往往最重要的是快速上線前域,時(shí)間非常緊張辕近,這時(shí)容易出現(xiàn)單測(cè)代碼難以編寫(xiě),代碼經(jīng)常變化導(dǎo)致單測(cè)代碼也需要更著同步變化匿垄,一定程度上會(huì)拖慢項(xiàng)目的進(jìn)度移宅,可以在項(xiàng)目中后期再補(bǔ)上重要部分的單測(cè)代碼
- 推廣實(shí)行:項(xiàng)目中推廣單測(cè)有一定成本,單純?yōu)榱烁采w率的單測(cè)是沒(méi)什么意義的椿疗,所以在項(xiàng)目中推廣單測(cè)時(shí)漏峰,要考慮到項(xiàng)目成員是否接受單測(cè),能否編寫(xiě)出較好的單測(cè)代碼届榄,否則單測(cè)容易流于形式浅乔,達(dá)不到理想的效果。
個(gè)人經(jīng)驗(yàn)痒蓬,在項(xiàng)目中要施行單測(cè)童擎,需要做到以下幾點(diǎn):
- 說(shuō)服領(lǐng)導(dǎo),給出合理的考核指標(biāo)(如單測(cè)覆蓋率等要求攻晒,需要結(jié)合現(xiàn)狀給出合理的指標(biāo))
- 提供單測(cè)指標(biāo)統(tǒng)計(jì)的大盤,顯示項(xiàng)目單測(cè)指標(biāo)班挖,督促大家完成指標(biāo)
- 對(duì)項(xiàng)目結(jié)構(gòu)配置等進(jìn)行調(diào)整鲁捏,提供單測(cè)工具類,基礎(chǔ)類萧芙,讓單測(cè)易編寫(xiě)给梅,能運(yùn)行,速度快
- 對(duì)項(xiàng)目組成員進(jìn)行單測(cè)編寫(xiě)方法分享双揪,使成員熟悉單測(cè)技術(shù)
- 提供單測(cè)代碼示例动羽,示例要夠復(fù)雜,方便成員參考
- 定時(shí)檢查成員單測(cè)代碼渔期,提供改進(jìn)意見(jiàn)运吓,防止流于形式
單測(cè)工具介紹
JUnit
Java用的最多的單測(cè)框架渴邦,使用非常簡(jiǎn)單,主流IDE基本都集成了JUnit拘哨,具體用法就不介紹了谋梭,可以花十分鐘看看官方文檔http://junit.org/junit4/
Runner
JUnit的Runner是指繼承了org.junit.runner.Runner的類,是用來(lái)真正執(zhí)行單測(cè)方法的類倦青,JUnit有提供默認(rèn)的Runner瓮床,如org.junit.runners.JUnit4,也可以通過(guò)@RunWith注解來(lái)指定自定義的Runner产镐,如@RunWith(SpringRunner.class)
Runner的介紹可以參考這篇文章:
http://www.mscharhag.com/java/understanding-junits-runner-architecture
通過(guò)自定義Runner隘庄,可以簡(jiǎn)化單測(cè)的開(kāi)發(fā),例如癣亚,在Spring沒(méi)有提供Runner時(shí)峭沦,為Spring應(yīng)用編寫(xiě)單測(cè),需要自己初始化Spring上下文逃糟,然后從上下文中獲取要測(cè)試的Bean吼鱼,使用起來(lái)比較麻煩。
對(duì)于自定義Runner來(lái)說(shuō)绰咽,一般會(huì)繼承JUnit的org.junit.runners.BlockJUnit4ClassRunner菇肃,BlockJUnit4ClassRunner主要提供了下面的功能:
- 反射創(chuàng)建單測(cè)類實(shí)例
- 找出單測(cè)方法(被@Test注解的方法)
- 其它JUnit注解支持,如@After取募、@Before等等
- 反射執(zhí)行單測(cè)方法
SpringRunner通過(guò)繼承BlockJUnit4ClassRunner琐谤,在其基礎(chǔ)上提供了自動(dòng)初始化Spring上下文,單測(cè)類中的@Resource等注解的處理玩敏,等等一系列方便編寫(xiě)Spring單測(cè)的功能斗忌。
如果我們想實(shí)現(xiàn)在單測(cè)方法前后執(zhí)行一些邏輯,我們除了可以使用@Before注解旺聚,還可以通過(guò)實(shí)現(xiàn)自定Runner來(lái)實(shí)現(xiàn):
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
public class TestRunner extends BlockJUnit4ClassRunner {
public TestRunner(Class<?> klass) throws InitializationError {
super(klass);
}
@Override
protected Statement methodBlock(FrameworkMethod method) {
//FrameworkMethod 是單測(cè)方法的包裝類
//獲取父類處理的結(jié)果织阳,以便使用JUnit提供的注解的功能
Statement block = super.methodBlock(method);
//自定義的邏輯
Statement newBlock = new Statement() {
@Override
public void evaluate() throws Throwable {
//這里可以在單測(cè)方法執(zhí)行前做一些自定義邏輯
System.out.println("TestRunner before : " + method.getName());
block.evaluate();//單測(cè)方法執(zhí)行,包含@Before等注解處理邏輯
//這里可以在單測(cè)方法執(zhí)行后做一些自定義邏輯
System.out.println("TestRunner after : " + method.getName());
}
};
return newBlock;
}
}
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(TestRunner.class)
public class BTestClass {
@Test
public void test(){
System.out.println("test b");
}
}
//運(yùn)行輸出
//TestRunner before : test
//test b
//TestRunner after : test
在JUnit內(nèi)部砰粹,其實(shí)就是用類似的方式來(lái)實(shí)現(xiàn)@Before唧躲、@After等等注解功能的,通過(guò)層層的包裝Statement類碱璃,來(lái)實(shí)現(xiàn)功能的擴(kuò)展弄痹。
Rule
Rule是JUnit4.7新增加的功能,是JUnit的另一種擴(kuò)展機(jī)制嵌器,可以擴(kuò)展單測(cè)方法的執(zhí)行肛真。上面TestRunner的功能也可以通過(guò)Rule機(jī)制來(lái)實(shí)現(xiàn):
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class EchoRule implements TestRule {
@Override
public Statement apply(Statement base, Description description) {
Statement newBlock = new Statement() {
@Override
public void evaluate() throws Throwable {
//這里可以在單測(cè)方法執(zhí)行前做一些自定義邏輯
System.out.println("EchoRule before : " + description.getMethodName());
base.evaluate();//單測(cè)方法執(zhí)行,包含@Before等注解處理邏輯
//這里可以在單測(cè)方法執(zhí)行后做一些自定義邏輯
System.out.println("EchoRule after : " + description.getMethodName());
}
};
return newBlock;
}
}
import org.junit.Rule;
import org.junit.Test;
public class BTestClass {
@Rule
public EchoRule rule = new EchoRule();//必需要是public的
@Test
public void test() {
System.out.println("test b");
}
}
//輸出
//EchoRule before : test
//test b
//EchoRule after : test
可以看到Rule更多的是對(duì)單測(cè)方法執(zhí)行前后的一些邏輯的擴(kuò)展爽航,@Rule注解的屬性必需是public的實(shí)例屬性蚓让,如果想在所有單測(cè)方法執(zhí)行前后進(jìn)行處理(類似@BeforeClass乾忱、@AfterClass邏輯),可以通過(guò)@ClassRule注解來(lái)做到凭疮,被@ClassRule的屬性必需是static public的屬性
Rule機(jī)制相對(duì)Runner的好處在于饭耳,Runner只能指定一個(gè),而一個(gè)單測(cè)類可以指定多個(gè)Rule执解,Spring也有Rule的實(shí)現(xiàn)寞肖,在即想使用其它框架的Runner又想使用Spring的單測(cè)擴(kuò)展時(shí),可以使用其它框架的Runner衰腌,然后使用Spring的Rule新蟆,來(lái)組合使用,如:
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration(locations="classpath:spring-root.xml")
public class BTestClass {
@ClassRule
public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
@Test
public void test() {
System.out.println("test b");
}
}
JUnit自帶了一些方便使用的Rule實(shí)現(xiàn)右蕊,可以參考下面的文檔
https://github.com/junit-team/junit4/wiki/Rules
Mock框架
在真實(shí)項(xiàng)目中琼稻,往往需要依賴很多外部的接口,如HSF等接口饶囚,而我們?cè)谶\(yùn)行單測(cè)的時(shí)候RPC接口可能還未開(kāi)發(fā)完成或者因?yàn)榄h(huán)境問(wèn)題帕翻,無(wú)法訪問(wèn),這時(shí)我們想要測(cè)試自己部分的邏輯萝风,就需要使用到Mock框架嘀掸,來(lái)屏蔽掉外部系統(tǒng)的影響。
使用Mock通常會(huì)帶來(lái)以下一些好處:
- 隔絕其他模塊出錯(cuò)引起本模塊的測(cè)試錯(cuò)誤
- 隔絕其他模塊的開(kāi)發(fā)狀態(tài)规惰,只要定義好接口睬塌,不用管他們開(kāi)發(fā)有沒(méi)有完成
- 一些速度較慢的操作,可以用Mock Object代替歇万,使單測(cè)快速返回
- 隔離環(huán)境對(duì)單測(cè)執(zhí)行的影響揩晴,實(shí)現(xiàn)在沒(méi)有外部服務(wù)時(shí)也能運(yùn)行單測(cè)
常見(jiàn)的Mock框架有EasyMock、Mocktio贪磺、JMockit硫兰、PowerMock等,個(gè)人只簡(jiǎn)單用過(guò)Mocktio和JMockit缘挽,就功能上瞄崇,JMockit的功能更強(qiáng),能Mock靜態(tài)方法等壕曼,但是根據(jù)之前的使用來(lái)看,比較難以駕馭等浊,因?yàn)镴Mockit使用的是Java5的Instrumentation機(jī)制腮郊,會(huì)在運(yùn)行時(shí)修改字節(jié)碼,導(dǎo)致碰到問(wèn)題時(shí)比較難以調(diào)試筹燕,相對(duì)的網(wǎng)絡(luò)資料也比較少轧飞,而Mocktio的使用比較簡(jiǎn)單明了衅鹿,因此推薦使用Mocktio
關(guān)于Mocktio的使用可以參考文檔:
[https://static.javadoc.io/org.mockito/mockito-core/2.7.22/org/mockito/Mockito.html
https://juejin.im/entry/578f11aec4c971005e0caf82](https://static.javadoc.io/org.mockito/mockito-core/2.7.22/org/mockito/Mockito.html
https://juejin.im/entry/578f11aec4c971005e0caf82)
因?yàn)镸ocktio是使用Cglib來(lái)創(chuàng)建代理的,所以對(duì)于被Mock的對(duì)象來(lái)說(shuō)过咬,要求和Cglib創(chuàng)建代理的要求一樣大渤,如不能是final類、不能代理private方法等等限制
有時(shí)候可能需要mock void方法掸绞,可以使用下面的方式
OssCache cache = Mockito.mock(OssCache.class);
Mockito.doNothing().when(cache).putToKey("key", "val");
Mockito.doThrow(new RuntimeException("exp")).when(cache).deleteObj("key");
cache.putToKey("key", "val");
try {
cache.deleteObj("key");
fail("mast throw exception");
} catch (RuntimeException e) {
assertTrue("exp".equals(e.getMessage()));
}
Mockito.verify(cache).putToKey("key", "val");
Mockito.verify(cache).deleteObj("key");
當(dāng)Mockito的注解使用起來(lái)比較方便泵三,具體注解的使用參見(jiàn)前面鏈接的文檔,Mockito處理注解是通過(guò)MockitoAnnotations.initMocks(target)來(lái)處理的衔掸,而Mockito提供的Runner和Rule其實(shí)就是簡(jiǎn)單的在單測(cè)方法執(zhí)行前執(zhí)行該行代碼烫幕,所以可以通過(guò)@RunWith(MockitoJUnitRunner.class)或@Rule public MockitoRule mockitoRule = MockitoJUnit.rule()方式來(lái)使用Mockito注解功能,當(dāng)和Spring Test一起使用時(shí)敞映,因?yàn)橐话銜?huì)使用Spring的Runner较曼,所以可以通過(guò)Rule的方式來(lái)使用Mockito的注解功能
關(guān)于Mockito的大概原理如下
主要通過(guò)ThreadLocal將我們要mock的方法和對(duì)應(yīng)的返回值關(guān)聯(lián)起來(lái)
內(nèi)存數(shù)據(jù)庫(kù)
在項(xiàng)目中經(jīng)常會(huì)使用到Mysql等數(shù)據(jù)庫(kù),但是在單測(cè)運(yùn)行時(shí)振愿,如果訪問(wèn)Mysql等外部服務(wù)器捷犹,會(huì)造成:
- 單測(cè)運(yùn)行慢
- 單測(cè)運(yùn)行依賴環(huán)境,在無(wú)法訪問(wèn)Mysql時(shí)冕末,單測(cè)無(wú)法運(yùn)行
- 單測(cè)可能會(huì)運(yùn)行的非常頻繁萍歉,造成Mysql中非常多的垃圾數(shù)據(jù)
- 單測(cè)依賴數(shù)據(jù)庫(kù)中某些特定的數(shù)據(jù),造成換個(gè)Mysql數(shù)據(jù)庫(kù)時(shí)單測(cè)運(yùn)行失敗
那么如何解決上面的問(wèn)題栓霜,一種方式是Mock掉所有DAO的類翠桦,這種方式需要寫(xiě)非常多的Mock,單測(cè)寫(xiě)起來(lái)比較麻煩胳蛮,且DAO層面問(wèn)題無(wú)法測(cè)試到销凑;另一種方式就是使用內(nèi)存數(shù)據(jù)庫(kù),內(nèi)存數(shù)據(jù)庫(kù)兼容SQL仅炊,啟動(dòng)速度快斗幼,數(shù)據(jù)存放在內(nèi)存中單測(cè)運(yùn)行后自動(dòng)丟棄,非常適合單測(cè)時(shí)使用
常見(jiàn)的內(nèi)存數(shù)據(jù)庫(kù)有很多抚垄,但是鑒于單測(cè)場(chǎng)景蜕窿,考慮到安裝方便(直接Maven依賴),了解到的有HSQL呆馁、H2桐经、Derby等,H2的官網(wǎng)上有個(gè)對(duì)比表http://www.h2database.com/html/main.html
考慮到目前我們使用的是Mysql數(shù)據(jù)庫(kù)浙滤,而H2有Mysql模式阴挣,對(duì)Mysql的語(yǔ)法支持的最好,所以建議使用H2數(shù)據(jù)庫(kù)來(lái)作為單測(cè)數(shù)據(jù)庫(kù)纺腊,但是H2并不支持所有的Mysql語(yǔ)法畔咧,還是有不少的Mysql語(yǔ)法或函數(shù)并不支持茎芭,對(duì)于建表語(yǔ)句而言,可以使用語(yǔ)法轉(zhuǎn)換工具https://github.com/bgranvea/mysql2h2-converter
Spring對(duì)嵌入式數(shù)據(jù)庫(kù)支持的非常好誓沸,可以通過(guò)下面的配置來(lái)創(chuàng)建嵌入式數(shù)據(jù)庫(kù)數(shù)據(jù)源梅桩,同時(shí)可以指定初始化表和數(shù)據(jù)庫(kù)的腳本
<jdbc:embedded-database id="dataSource" generate-name="true" type="H2">
<jdbc:script location="classpath:/sql/test_schema.sql" />
<jdbc:script location="classpath:/sql/test_init_data.sql" />
</jdbc:embedded-database>
具體使用可以參考Spring的文檔https://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-embedded-database-support
除了H2,還可以使用MariaDB4j拜隧,MariaDB的Java包裝版本(用Java代碼安裝MariaDB精簡(jiǎn)版然后啟動(dòng)宿百,比較重)
https://github.com/vorburger/MariaDB4j
Spring Test介紹
當(dāng)應(yīng)用使用了Spring時(shí),編寫(xiě)單測(cè)時(shí)需要每次手動(dòng)的初始化Spring上下文虹蓄,這種方式不僅繁瑣犀呼,而且不能復(fù)用Spring上下文,導(dǎo)致單測(cè)執(zhí)行時(shí)間變長(zhǎng)薇组,為此外臂,Spring提供了對(duì)單測(cè)的支持,也就是Spring Test模塊
Spring和JUnit的整合律胀,提供了對(duì)應(yīng)的Runner和Rule宋光,我們平常使用的比較多的是Spring的Runner,即SpringJUnit4ClassRunner或者SpringRunner(Spring4.3)炭菌,Spring的Runner會(huì)根據(jù)配置自動(dòng)初始化Spring上下文罪佳,并在單測(cè)方法執(zhí)行時(shí)對(duì)其進(jìn)行依賴注入,避免手動(dòng)的getBean操作黑低,簡(jiǎn)單使用如下
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-test-main.xml")
public class Test {
@Resource
private SomeBean bean;
@Test
public void test(){
String someVal = bean.someMethod();
}
}
Spring Test提供@ContextConfiguration來(lái)讓我們指定要初始Spring上下文的配置赘艳,支持Spring的各種配置方式,如XML克握、JavaConfig等等方式蕾管,@ContextConfiguration和@RunWith等注解都可以注解在基類上,所以可以提供一個(gè)基礎(chǔ)類來(lái)簡(jiǎn)化單測(cè)的編寫(xiě)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:base-context.xml")
public class XXXTestBase {
}
@ContextConfiguration("classpath:extended-context.xml")
public class YYYTest extends XXXTestBase{
}
默認(rèn)情況下菩暗,子類可以基礎(chǔ)父類的@ContextConfiguration配置掰曾,同時(shí)可以追加自己的配置,當(dāng)程序非常模塊化時(shí)停团,可以通過(guò)指定特定的配置文件來(lái)減少初始化Bean的數(shù)量旷坦,以便提高單測(cè)的執(zhí)行速度
@TestExecutionListeners注解是Spring Test用來(lái)注冊(cè)TestExecutionListener的注解,提供的類似于JUnit的Before佑稠、After的擴(kuò)展方法秒梅,父類的@TestExecutionListeners注解配置通樣可以被子類基礎(chǔ),子類也可以提供自己個(gè)性的@TestExecutionListeners配置
//可以通過(guò)@Order指定TestExecutionListener的順序
public interface TestExecutionListener {
void beforeTestClass(TestContext testContext) throws Exception;
void prepareTestInstance(TestContext testContext) throws Exception;
void beforeTestMethod(TestContext testContext) throws Exception;
void afterTestMethod(TestContext testContext) throws Exception;
void afterTestClass(TestContext testContext) throws Exception;
}
單測(cè)一般不需要顯示的配置@TestExecutionListeners注解舌胶,默認(rèn)@ContextConfiguration會(huì)自動(dòng)注冊(cè)如下Spring Test自帶的TestExecutionListener
org.springframework.test.context.web.ServletTestExecutionListener
org.springframework.test.context.support.DependencyInjectionTestExecutionListener
org.springframework.test.context.support.DirtiesContextTestExecutionListener
org.springframework.test.context.transaction.TransactionalTestExecutionListener
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener
可以看到番电,Spring Test很多方便的功能都是通過(guò)TestExecutionListener來(lái)實(shí)現(xiàn)的,比如說(shuō)DependencyInjectionTestExecutionListener來(lái)為單測(cè)類實(shí)例進(jìn)行依賴注入的
public class DependencyInjectionTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void prepareTestInstance(final TestContext testContext) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("Performing dependency injection for test context [" + testContext + "].");
}
injectDependencies(testContext);
}
protected void injectDependencies(final TestContext testContext) throws Exception {
Object bean = testContext.getTestInstance();
AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
beanFactory.initializeBean(bean, testContext.getTestClass().getName());
testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
}
}
在單測(cè)中經(jīng)常會(huì)使用到Mockito的注解辆琅,所以可以在單測(cè)基礎(chǔ)類中使用Mockito的Rule漱办,這樣子類就可以使用Mockito的注解了
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:base-context.xml")
public class XXXTestBase {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
}
@ContextConfiguration("classpath:extended-context.xml")
public class YYYTest extends XXXTestBase{
@Resource
@@InjectMocks
private SpringXXXBean xxxBean;
@Mock
private XXXHsfBean xxxHsfBean
}
在執(zhí)行單測(cè)時(shí),涉及到數(shù)據(jù)庫(kù)操作時(shí)經(jīng)常要在單測(cè)方法執(zhí)行前在數(shù)據(jù)庫(kù)中準(zhǔn)備好單測(cè)數(shù)據(jù)婉烟,Spring Test提供了非常方便的注解來(lái)在單測(cè)方法前初始化數(shù)據(jù)娩井,如下所示
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-test-root.xml")
public class BTestClass {
@Resource
private TestMapper mapper;
@Test
@Sql("BTestClass_testAAA.sql")
//默認(rèn)會(huì)在BTestClass相同的目錄下查找BTestClass_testAAA.sql文件執(zhí)行
public void testAAA() {
List<TestModel> all = mapper.getAll();
Assert.isTrue(all != null && all.size() == 1);
}
}
@Sql注解可以注解在類上,表示每個(gè)單測(cè)方法前都執(zhí)行該SQL腳本似袁,但是要注意單測(cè)方法插入到數(shù)據(jù)庫(kù)的記錄默認(rèn)并不會(huì)在單測(cè)執(zhí)行完后回滾洞辣,所以如果SQL腳本中有插入操作,容易出現(xiàn)主鍵沖突昙衅,因?yàn)槟_本會(huì)在每次單測(cè)執(zhí)行時(shí)都執(zhí)行
@Sql可以指定腳本的執(zhí)行時(shí)機(jī)扬霜,如在單測(cè)方法執(zhí)行前或執(zhí)行后,通過(guò)executionPhase參數(shù)控制
在單測(cè)時(shí)而涉,可能某些單測(cè)只能依賴MySQL著瓶,可以通過(guò)Spring的Profile功能來(lái)實(shí)現(xiàn)默認(rèn)使用H2,但可以通過(guò)注解的方式來(lái)顯示給某些單測(cè)指使用MYSQL數(shù)據(jù)源
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.2.xsd">
<beans profile="H2">
<jdbc:embedded-database id="dataSource" generate-name="true" type="H2">
<jdbc:script location="classpath:/sql/test_schema.sql" />
</jdbc:embedded-database>
</beans>
<beans profile="MYSQL">
<bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${db.url}" />
<property name="username" value="${db.username}" />
<property name="password" value="${db.password}" />
<!-- mysql jdbc 6.0的driver改包名了 -->
<property name="driverClassName" value="${db.driver.class}" />
<property name="initialSize" value="1" />
<property name="maxActive" value="50" />
<property name="minIdle" value="1" />
<property name="maxWait" value="60000" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="25200000" />
<property name="removeAbandoned" value="true" />
<property name="removeAbandonedTimeout" value="1800" />
<property name="logAbandoned" value="true" />
<property name="filters" value="mergeStat" />
</bean>
</beans>
</beans>
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-test-root.xml")
@ActiveProfiles(profiles = "H2", inheritProfiles = false)
public class BaseTest {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
}
@ActiveProfiles(profiles = "MYSQL", inheritProfiles = false)
public class BTestClass extends BaseTest {
@Resource
private TestMapper mapper;
@Test
@Sql("BTestClass_testAAA.sql")
public void testAAA() {
List<TestModel> all = mapper.getAll();
Assert.isTrue(all != null && all.size() == 1);
}
}
可以通過(guò)Spring元注解功能來(lái)使代碼更語(yǔ)義化一些
/**
* 選擇使用H2數(shù)據(jù)庫(kù)還是使用Mysql數(shù)據(jù)庫(kù), 底層使用的是Spring的Profile功能
*
* 可以通過(guò)
*
* @see spring-test-datasource.xml
* @author tudesheng
* @since 2016年9月13日 下午7:02:38
*
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ActiveProfiles(inheritProfiles = false)
public @interface DBSelecter {
/**
* profiles, 取值只能是DBTypes.H2, 或者是DBTypes.MYSQL, 默認(rèn)DBTypes.H2
*/
@AliasFor(annotation = ActiveProfiles.class, attribute = "profiles")
String[] value() default { DBTypes.H2 };
/**
* profiles, 取值只能是DBTypes.H2, 或者是DBTypes.MYSQL, 默認(rèn)DBTypes.H2
*/
@AliasFor(annotation = ActiveProfiles.class, attribute = "profiles")
String[] type() default { DBTypes.H2 };
}
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-test-root.xml")
@DBSelecter(DBTypes.H2)
public class BaseTest {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
}
@DBSelecter(DBTypes.MYSQL)
public class BTestClass extends BaseTest {
@Resource
private TestMapper mapper;
@Test
@Sql("BTestClass_testAAA.sql")
public void testAAA() {
List<TestModel> all = mapper.getAll();
Assert.isTrue(all != null && all.size() == 1);
}
}
在使用MYSQL等外部數(shù)據(jù)庫(kù)時(shí)啼县,單測(cè)的執(zhí)行很容易產(chǎn)生臟數(shù)據(jù)材原,可以通過(guò)@Rollback注解來(lái)標(biāo)注單測(cè)方法執(zhí)行完后,回滾數(shù)據(jù)庫(kù)操作季眷,減少對(duì)外部測(cè)試數(shù)據(jù)庫(kù)的污染
最后余蟹,某些單測(cè)方法執(zhí)行后可能會(huì)污染Spring的上下文,比如通過(guò)反射將某個(gè)容器內(nèi)的Bean的屬性給替換調(diào)了子刮,可能會(huì)對(duì)其它的單測(cè)造成影響威酒,這個(gè)時(shí)候,可以通過(guò)@DirtiesContext標(biāo)注該單測(cè)方法會(huì)污染Spring上下文挺峡,需要在單測(cè)執(zhí)行前或執(zhí)行后重新初始化Spring上下文葵孤,慎用,容易增加單測(cè)的執(zhí)行時(shí)間
常用工具類
Spring的AopTestUtils:提供了一些獲取原始代理對(duì)象的接口
Spring的ReflectionTestUtils:提供了一些反射的方便方法沙郭,可以用少量的代碼來(lái)反射修改參數(shù)熟悉
工程實(shí)踐
Demo工程
參考demo工程
http://gitlab.alibaba-inc.com/shengde.tds/unit_test_demo
問(wèn)題和經(jīng)驗(yàn)
Spring Test上下文污染問(wèn)題:
某些單測(cè)執(zhí)行時(shí)可能會(huì)污染Spring上下文佛呻,如通過(guò)反射修改了Spring上下文中的某些單例對(duì)象
可以通過(guò)@DirtiesContext標(biāo)記單測(cè)方法,促使Spring Test重新初始Spring上下文-
時(shí)間問(wèn)題
單測(cè)類中經(jīng)常有些方法是和時(shí)間有關(guān)的病线,不太好測(cè)試吓著,這時(shí)需要將時(shí)間獲取API抽象出來(lái)
//這個(gè)放在src/main/java中,并替換代碼中所有獲取當(dāng)前時(shí)間代碼為Time.currentTimeMillis()public abstract class Time { private static Time time = new Time() { @Override public long currentTime() { return System.currentTimeMillis(); } }; public static long currentTimeMillis() { return time.currentTime(); } public abstract long currentTime(); } //這個(gè)放在src/test/java中送挑,專供單測(cè)使用绑莺,在單測(cè)時(shí),通過(guò)反射替換掉Time.time靜態(tài)變量惕耕,就可以做到時(shí)間可控了 public class SettableTime extends Time { private long time = 0; public SettableTime(long time) { super(); this.time = time; } public SettableTime setTime(long time) { this.time = time; return this; } @Override public long currentTime() { return time; } }
-
外部RPC服務(wù)依賴問(wèn)題
現(xiàn)在項(xiàng)目中有很多外部的服務(wù)依賴纺裁,在Spring初始化上下文時(shí)需要初始化RPC相關(guān)的Bean和服務(wù),這會(huì)導(dǎo)致單測(cè)執(zhí)行慢、單測(cè)執(zhí)行依賴外部服務(wù)的穩(wěn)定等問(wèn)題欺缘,所以我們希望在單測(cè)的時(shí)候不使用外部的服務(wù)栋豫,同時(shí)也不對(duì)外提供RPC等服務(wù),可以將外部RPC服務(wù)的定義到單獨(dú)的Spring配置文件谚殊,然后在單測(cè)中將這些服務(wù)定義用Mock實(shí)現(xiàn)來(lái)替換丧鸯,HSF通過(guò)java接口來(lái)定義服務(wù),所以可以很方便的用cglib來(lái)創(chuàng)建代理類嫩絮,替換掉HSF的HSFSpringConsumerBean對(duì)象丛肢,如:/** * 外部接口mock類, 方便測(cè)試 * * * @since 2016年6月27日 上午11:21:08 * */ public class MockerFactory<T> implements FactoryBean<T> { private static final Logger logger = LoggerFactory.getLogger(MockerFactory.class); private Enhancer enhancer = new Enhancer(); private Class<T> clazz; public MockerFactory(Class<T> clazz) { this.clazz = clazz; } @Override @SuppressWarnings("unchecked") public T getObject() throws Exception { this.enhancer.setCallback(new MockerInterceptor()); this.enhancer.setSuperclass(clazz); return (T) this.enhancer.create(); } @Override public Class<?> getObjectType() { return clazz; } @Override public boolean isSingleton() { return true; } private class MockerInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { logger.info("調(diào)用Mock, 類:{}, 方法:{}", clazz.getSimpleName(), method.getName()); return null; } } } //xml <bean class="com.didichuxing.lang.atlantic.srv.test.util.MockerFactory"> <constructor-arg index="0" value="xxx.hsf.interface" /> </bean> //這里可以修改MockerFactory的代碼,使其的參數(shù)和構(gòu)造方式修改為HSFSpringConsumerBean //這樣可以簡(jiǎn)單的替換掉配置文件中的HSFSpringConsumerBean即可
一些會(huì)通過(guò)afterPropertiesSet等初始化外部資源的bean
某些Spring Bean會(huì)通過(guò)afterPropertiesSet等方法中初始化MQ獲取其它外部的依賴剿干,通常不要將這些類配置為注解方式蜂怎,而是通過(guò)XML配置的方式來(lái)使用,這樣單測(cè)的時(shí)候可以非常方便的將其Mock掉置尔,保證單測(cè)簡(jiǎn)單快速的執(zhí)行
盡量保證Spring配置文件的模塊化杠步,如將HSF消費(fèi)者和HSF提供者單獨(dú)使用一個(gè)Spring配置文件,這樣單測(cè)只用替換少量的配置即可撰洗,不用總是維護(hù)兩套配置文件篮愉,同時(shí)執(zhí)行單測(cè)時(shí)也可以選擇只初始化少量模塊,加快單測(cè)的執(zhí)行速度
AutoConfig問(wèn)題差导,在阿里非常多的應(yīng)用使用AutoConfig试躏,但是使用AutoConfig后,因?yàn)榕渲梦募窃诖虬鼤r(shí)替換设褐,導(dǎo)致啟動(dòng)時(shí)比較麻煩颠蕴,可以通過(guò)將AutoConfig的屬性集中成一個(gè)properties文件,使用AutoConfig替換該properties文件助析,然后Spring再使用placeholder的方式來(lái)引入被替換的properties文件犀被,這樣,在單測(cè)的時(shí)候外冀,可以方便的使用自己的配置文件來(lái)替換掉需要AutoConfig才能替換的文件
其它單測(cè)框架