單元測(cè)試

單元測(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è)框架

http://blog.2baxb.me/archives/1398

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寡键,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子雪隧,更是在濱河造成了極大的恐慌西轩,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脑沿,死亡現(xiàn)場(chǎng)離奇詭異藕畔,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)庄拇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門注服,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)韭邓,“玉大人,你說(shuō)我怎么就攤上這事溶弟∨纾” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵可很,是天一觀的道長(zhǎng)诗力。 經(jīng)常有香客問(wèn)我,道長(zhǎng)我抠,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任袜茧,我火速辦了婚禮菜拓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘笛厦。我一直安慰自己纳鼎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布裳凸。 她就那樣靜靜地躺著贱鄙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪姨谷。 梳的紋絲不亂的頭發(fā)上逗宁,一...
    開(kāi)封第一講書(shū)人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音梦湘,去河邊找鬼瞎颗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛捌议,可吹牛的內(nèi)容都是我干的哼拔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼瓣颅,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼倦逐!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起宫补,我...
    開(kāi)封第一講書(shū)人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤檬姥,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后守谓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體穿铆,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年斋荞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了荞雏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖凤优,靈堂內(nèi)的尸體忽然破棺而出悦陋,到底是詐尸還是另有隱情,我是刑警寧澤筑辨,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布俺驶,位于F島的核電站,受9級(jí)特大地震影響棍辕,放射性物質(zhì)發(fā)生泄漏暮现。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一楚昭、第九天 我趴在偏房一處隱蔽的房頂上張望栖袋。 院中可真熱鬧,春花似錦抚太、人聲如沸塘幅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)电媳。三九已至,卻和暖如春庆亡,著一層夾襖步出監(jiān)牢的瞬間匾乓,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工身冀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钝尸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓搂根,卻偏偏與公主長(zhǎng)得像珍促,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子剩愧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理猪叙,服務(wù)發(fā)現(xiàn),斷路器仁卷,智...
    卡卡羅2017閱讀 134,652評(píng)論 18 139
  • 在博客Android單元測(cè)試之JUnit4中穴翩,我們簡(jiǎn)單地介紹了:什么是單元測(cè)試,為什么要用單元測(cè)試锦积,并展示了一個(gè)簡(jiǎn)...
    水木飛雪閱讀 9,444評(píng)論 4 18
  • 本文介紹了Android單元測(cè)試入門所需了解的內(nèi)容芒帕,包括JUnit、Mockito和PowerMock的使用丰介,怎樣...
    于衛(wèi)國(guó)閱讀 4,573評(píng)論 0 5
  • 什么是單元測(cè)試 在計(jì)算機(jī)編程中背蟆,單元測(cè)試(Unit Testing)又稱為模塊測(cè)試, 是針對(duì)程序模塊(軟件設(shè)計(jì)的最...
    HelloCsl閱讀 10,953評(píng)論 1 46
  • 孔子說(shuō)鉴分,不學(xué)禮無(wú)以立。因此带膀,想讓孩子成為一個(gè)風(fēng)度翩翩志珍,彬彬有禮的人,從小就要培養(yǎng)他良好的禮儀習(xí)慣垛叨。行為舉止優(yōu)雅得體...
    果實(shí)禮儀閱讀 1,116評(píng)論 4 1