單元測試之JUnit4

JUnit4

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

Assertions斷言

判斷結(jié)果是否滿足預(yù)期,Junit有以下幾種斷言方法:assertArrayEqualsassertEquals、assertFalse、assertNotNull、assertNotSame、assertNull、assertSame、assertTrueassertThat。

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相當于是相對測試方法獨立的作用于測試方法中的額外處理邏輯。多個Rule可以順序疊加。如果一個規(guī)則標注為@Rule則對測試類的每個方法生效,如果一個規(guī)則標注為@ClassRule則只會在整個測試類的所有方法開始之前和結(jié)束只會生效一次。以下常見的Rules如下:

Rules名稱 作用 備注
ErrorCollector 使用ErrorCollector.checkThat()方法可以在執(zhí)行完整個測試方法之后再報錯,不會因為測試方法中的某一個錯誤而提前終止測試 JUnit自帶 文檔
ExpectedException 使用ExpectedException.expect()方法指定測試方法需要拋出的異常,當測試方法沒有拋出異?;蛘邟仐壊环项A(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中為當前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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容