單元測試之JUnit4

JUnit4

JUnit是一個幫助編寫和執(zhí)行單元測試的框架梅肤。可能很多人都接觸過單元測試,但是只是停留在copy別人的測試代碼再改一下的狀態(tài)呆瞻,下文嘗試較為體系列舉JUnit4中比較關(guān)鍵的一些知識點谷婆。轉(zhuǎn)載請注明來源「Bug總柴」

Assertions斷言

判斷結(jié)果是否滿足預(yù)期慨蛙,Junit有以下幾種斷言方法:assertArrayEqualsassertEquals纪挎、assertFalse期贫、assertNotNullassertNotSame异袄、assertNull通砍、assertSameassertTrue烤蜕、assertThat封孙。

Hamcrest Mathers

Hamcrest擴展JUnitassertThat的Matcher類型,支持以下matchers:

支持類型 常用matchers
Core anything讽营、describedAs虎忌、is
Logical allOf僚匆、anyOf宙址、not、both届榄、either
Object equalTo、hasToString挑围、instanceOf礁竞、isCompatibleType、notNullValue贪惹、nullValue苏章、sameInstance、theInstance
Beans hasProperty
Collections array奏瞬、hasEntry枫绅、hasKey、hasValue硼端、hasItem并淋、hasItems、hasItemInArray珍昨、everyItem
Number closeTo县耽、greaterThan、greaterThanOrEqualTo镣典、lessThan兔毙、lessThanOrEqualTo
Text equalToIgnoringCase、equalToIgnoringWhiteSpace兄春、containsString澎剥、endsWith、startsWith

使用assertThat具有更好的可讀性和出錯信息赶舆,建議大多數(shù)情況下使用assertTaht來進行斷言判斷哑姚。

Runner執(zhí)行器

Runner執(zhí)行器用于組織和執(zhí)行在一個類中的測試,可以在執(zhí)行器中做一些必須的前置和后置工作芜茵。使用@RunWith注解可以指定測試的執(zhí)行類叙量。如果沒有使用@RunWith指定測試執(zhí)行器,默認會使用BlockJunit4ClassRunner九串。每個測試類只能指定一個Runner绞佩。除了默認的Runner還有以下的Runner:

Runner名稱 作用 備注
Suite 將分布在多個類中的測試組合在一起作為一個測試執(zhí)行 JUnit自帶 文檔
Categories 執(zhí)行多個類具有某些類別標志的一組測試 JUnit自帶 文檔
Parameterized 使用同一種類型的多個數(shù)據(jù)重復(fù)執(zhí)行同一個測試類的所有測試 JUnit自帶 文檔
Theories 使用多種類型的數(shù)據(jù)的排列組合執(zhí)行同一個測試類的所有測試 JUnit自帶 文檔
SpringJUnit4ClassRunner 提供Spring上下文支持的測試執(zhí)行類 繼承自BlockJUnit4ClassRunner
MockitoJUnitRunner 最終會構(gòu)造DefaultInternalRunner,根據(jù)mockito的注解在測試之前生成mock對象 詳見mockito介紹
PowerMockRunner 最終通過PowerMockJUnit44RunnerDelegateImpl.executeTest()方法蒸辆,將被@PrepareForTest征炼、@PrepareOnlyThisForTest@SuppressStaticInitializationFor標注的類使用MockClassLoader進行加載躬贡,實現(xiàn)對靜態(tài)以及final對象的mock 詳見powermock文檔
AndroidJUnit4 會根據(jù)是否在Android設(shè)備執(zhí)行谆奥,選擇AndroidJUnit4ClassRunner或者RobolectricTestRunner(AndroidX版本)。在Android中執(zhí)行測試時拂玻,可以獲得運行的Instrumentation和Bundle參數(shù)酸些,以及可以使用@UiThreadTest標記測試方法在UI線程執(zhí)行 原理可以參閱這篇文章
RobolectricTestRunner 直接在安卓真機或者模擬器運行測試通常會比較慢宰译,RobolectricTestRunner繼承自SandboxTestRunner,以提供在JVM中的Android運行時環(huán)境 詳見Robolectric官網(wǎng)
其他 其他的Runner可以看這里

Rule規(guī)則

使用Rule可以對一個或者一組測試的方法進行修改魄懂,可以向測試方法中添加額外邏輯來決定測試是否通過沿侈,也可以代替@Before@After市栗、@BeforeClass缀拭、@AfterClass來實現(xiàn)初始化和清理工作。換句話而言填帽,Rule相當(dāng)于是相對測試方法獨立的作用于測試方法中的額外處理邏輯蛛淋。多個Rule可以順序疊加。如果一個規(guī)則標注為@Rule則對測試類的每個方法生效篡腌,如果一個規(guī)則標注為@ClassRule則只會在整個測試類的所有方法開始之前和結(jié)束只會生效一次褐荷。以下常見的Rules如下:

Rules名稱 作用 備注
ErrorCollector 使用ErrorCollector.checkThat()方法可以在執(zhí)行完整個測試方法之后再報錯,不會因為測試方法中的某一個錯誤而提前終止測試 JUnit自帶 文檔
ExpectedException 使用ExpectedException.expect()方法指定測試方法需要拋出的異常嘹悼,當(dāng)測試方法沒有拋出異撑迅Γ或者拋棄不符合預(yù)期的異常時判定測試失敗 JUnit自帶 文檔
ExternalResource 類似于@Before@After的效果,只是用了Rule來實現(xiàn)杨伙,可以聲明發(fā)生在測試之前和測試之后的行為 JUnit自帶文檔
TemporaryFolder 在測試方法之前創(chuàng)建一個存放測試臨時文件的目錄其监,在測試結(jié)束后會自動刪除 JUnit自帶 文檔
TestWatcher 可以用來監(jiān)測測試方法執(zhí)行的生命周期,包括開始限匣、成功棠赛、錯誤、結(jié)束等 JUnit自帶 文檔
TestName 繼承自TestWatcher膛腐,用來獲取每個測試方法的名字 JUnit自帶 文檔
Timeout 將測試類中的每個測試方法都是用獨立的線程執(zhí)行,并等待一段時間鼎俘。若等待時間內(nèi)沒有結(jié)果返回則報錯哲身。如果設(shè)置等待時間為0,則表示沒有超時只是在線程中執(zhí)行贸伐。 JUnit自帶 文檔
RuleChain 將多個Rule按照指定的順序作用于測試方法中 JUnit自帶 文檔
Verifier ErrorCollector的基類勘天,抽象類,表示可以在運行完測試方法后做一些驗證操作 JUnit自帶 文檔
MockitoRule 是一個擴展MethodRule的接口捉邢,通過JUnitRule實現(xiàn)脯丝,會在執(zhí)行測試方法之前,初始化所有mock對象伏伐。這個rule的作用與MockitoJUnitRunner類似 文檔
PowerMockRule 最終通過PowerMockAgentTestInitializer.initialize()方法將被@PrepareForTest宠进、@PrepareOnlyThisForTest、@SuppressStaticInitializationFor標注的類使用MockClassLoader進行加載藐翎,實現(xiàn)對靜態(tài)以及final對象的mock材蹬,作用與PowerMockRunner類似 文檔
ProviderTestRule 在測試方法之前對ContentProvider進行初始化实幕,可以執(zhí)行相應(yīng)的數(shù)據(jù)庫操作。 文檔
ServiceTestRule 調(diào)用ServiceTestRule.startService()或者ServiceTestRule.bindService()在測試方法中建立Service連接堤器,在測試結(jié)束后會自動關(guān)閉Service昆庇。不適用于IntentService,可以對其他Service進行測試闸溃。 文檔
ActivityTestRule 可以自動在測試方法和@Before之前啟動Activity整吆,并在測試方法結(jié)束和@After之后結(jié)束Activity。也可以手動調(diào)用ActivityTestRule.launchActivity()ActivityTestRule.finishActivity() 文檔
GrantPermissionRule 幫助在Android API 23及以上的環(huán)境申請運行時權(quán)限辉川。申請權(quán)限時可以避免用戶交互彈窗占用UI測試焦點表蝙。最終會調(diào)用PermissionRequester.requestPermissions()方法,通過執(zhí)行UiAutomationShellCommand直接在shell中為當(dāng)前target申請權(quán)限 文檔
ActivityScenarioRule 作為ActivityTestRule的替代员串,在測試方法之前啟動一個activity勇哗,并在測試方法之后結(jié)束activity。同時可以在測試方法中獲得ActivityScenario ActivityScenarioRule文檔 / ActivityScenario文檔
InstantTaskExecutorRule 用于Architecture Components的測試寸齐,可以將默認使用的后臺executor轉(zhuǎn)為同步執(zhí)行欲诺,讓測試可以馬上獲得結(jié)果 文檔
CountingTaskExecutorRule 可以使用CountingTaskExecutorRule.drainTasks()方法手動等待所有Architecture Components的后臺任務(wù)執(zhí)行完畢 文檔
IntentsTestRule 在測試之前會初始化Espresso的Intent,可以使用Espresso Intents.intended()方法校驗activity操作觸發(fā)的intent espresso intent

測試默認執(zhí)行流程源碼分析

整個測試的執(zhí)行過程是對Statement根據(jù)@BeforeClass渺鹦、@AfterClass扰法、@Before@After毅厚、Rules的按照裝飾者模式進行的層層包裝塞颁。最后會根據(jù)這些包裝的規(guī)則一步一步執(zhí)行測試。

// BlockJUnit4ClassRunner會繼承ParentRunner
public abstract class ParentRunner<T> extends Runner implements Filterable, Sortable {

    // 執(zhí)行測試
    @Override
    public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                getDescription());
        try {
            Statement statement = classBlock(notifier);
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.addFailedAssumption(e);
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            testNotifier.addFailure(e);
        }
    }
    
    // 在執(zhí)行類中測試的前后加上BeforeClass和AfterClass邏輯
    protected Statement classBlock(final RunNotifier notifier) {
        // 執(zhí)行測試類中的測試方法
        Statement statement = childrenInvoker(notifier);
        if (!areAllChildrenIgnored()) {
            // 這里會對類中的測試加上Before和After的邏輯
            statement = withBeforeClasses(statement);
            statement = withAfterClasses(statement);
            statement = withClassRules(statement);
        }
        return statement;
    }
    
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }
    
    private void runChildren(final RunNotifier notifier) {
        final RunnerScheduler currentScheduler = scheduler;
        try {
            for (final T each : getFilteredChildren()) {
                currentScheduler.schedule(new Runnable() {
                    public void run() {
                        // 最終先執(zhí)行runChild
                        ParentRunner.this.runChild(each, notifier);
                    }
                });
            }
        } finally {
            currentScheduler.finished();
        }
    }
}
// 默認JUnit4 Runner BlockJUnit4ClassRunner對runChild進行處理
public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {
    @Override
    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
        Description description = describeChild(method);
        if (isIgnored(method)) {
            notifier.fireTestIgnored(description);
        } else {
            // 調(diào)用methodBlock加入Before/After以及Rules邏輯
            runLeaf(methodBlock(method), description, notifier);
        }
    }
    
    // 最終會調(diào)用After/Before以及Rule邏輯
    protected Statement methodBlock(FrameworkMethod method) {
        Object test;
        try {
            test = new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return createTest();
                }
            }.run();
        } catch (Throwable e) {
            return new Fail(e);
        }

        Statement statement = methodInvoker(method, test);
        // 在測試的前后加上Before/After以及withRules
        statement = possiblyExpectingExceptions(method, test, statement);
        statement = withPotentialTimeout(method, test, statement);
        statement = withBefores(method, test, statement);
        statement = withAfters(method, test, statement);
        statement = withRules(method, test, statement);
        return statement;
    }
}
// 對于Rule規(guī)則吸耿,最終會調(diào)用TestRule.apply()方法
public class RunRules extends Statement {
    private final Statement statement;

    public RunRules(Statement base, Iterable<TestRule> rules, Description description) {
        statement = applyAll(base, rules, description);
    }

    @Override
    public void evaluate() throws Throwable {
        statement.evaluate();
    }

    private static Statement applyAll(Statement result, Iterable<TestRule> rules,
            Description description) {
        for (TestRule each : rules) {
            // 順序加上Rule的邏輯
            result = each.apply(result, description);
        }
        return result;
    }
}

JUnit后記

如果有能代替Runner的Rule祠锣,最好使用Rule,因為一個測試類可以指定多個Rule咽安,但是只能聲明一個Runner伴网。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市妆棒,隨后出現(xiàn)的幾起案子澡腾,更是在濱河造成了極大的恐慌,老刑警劉巖糕珊,帶你破解...
    沈念sama閱讀 194,670評論 5 460
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件动分,死亡現(xiàn)場離奇詭異,居然都是意外死亡红选,警方通過查閱死者的電腦和手機澜公,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 81,928評論 2 371
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纠脾,“玉大人玛瘸,你說我怎么就攤上這事蜕青。” “怎么了糊渊?”我有些...
    開封第一講書人閱讀 141,926評論 0 320
  • 文/不壞的土叔 我叫張陵右核,是天一觀的道長。 經(jīng)常有香客問我渺绒,道長贺喝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 52,238評論 1 263
  • 正文 為了忘掉前任宗兼,我火速辦了婚禮躏鱼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘殷绍。我一直安慰自己染苛,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 61,112評論 4 356
  • 文/花漫 我一把揭開白布主到。 她就那樣靜靜地躺著茶行,像睡著了一般。 火紅的嫁衣襯著肌膚如雪登钥。 梳的紋絲不亂的頭發(fā)上畔师,一...
    開封第一講書人閱讀 46,138評論 1 272
  • 那天,我揣著相機與錄音牧牢,去河邊找鬼看锉。 笑死,一個胖子當(dāng)著我的面吹牛塔鳍,可吹牛的內(nèi)容都是我干的伯铣。 我是一名探鬼主播,決...
    沈念sama閱讀 36,545評論 3 381
  • 文/蒼蘭香墨 我猛地睜開眼轮纫,長吁一口氣:“原來是場噩夢啊……” “哼懂傀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蜡感,我...
    開封第一講書人閱讀 35,232評論 0 253
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恃泪,沒想到半個月后郑兴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 39,496評論 1 290
  • 正文 獨居荒郊野嶺守林人離奇死亡贝乎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 34,596評論 2 310
  • 正文 我和宋清朗相戀三年情连,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片览效。...
    茶點故事閱讀 36,369評論 1 326
  • 序言:一個原本活蹦亂跳的男人離奇死亡却舀,死狀恐怖虫几,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情挽拔,我是刑警寧澤辆脸,帶...
    沈念sama閱讀 32,226評論 3 313
  • 正文 年R本政府宣布,位于F島的核電站螃诅,受9級特大地震影響啡氢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜术裸,卻給世界環(huán)境...
    茶點故事閱讀 37,600評論 3 299
  • 文/蒙蒙 一倘是、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧袭艺,春花似錦搀崭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 28,906評論 0 17
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至袍镀,卻和暖如春默蚌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背苇羡。 一陣腳步聲響...
    開封第一講書人閱讀 30,185評論 1 250
  • 我被黑心中介騙來泰國打工绸吸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人设江。 一個月前我還...
    沈念sama閱讀 41,516評論 2 341
  • 正文 我出身青樓锦茁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叉存。 傳聞我的和親對象是個殘疾皇子码俩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 40,721評論 2 335