C++11 模板元編程 - 測(cè)試Teardown


既然fixture內(nèi)部可以定義setup,我們自然希望也能對(duì)稱地定義teardown,用于處理同一個(gè)fixture中所有測(cè)試用例共同的善后工作。

比如我們希望有如下的測(cè)試用例描述方式:

FIXTURE(TestTearDown)
{
    TEARDOWN()
    {
        ASSERT_EQ(__int(0), expected);
    }

    TEST("test 1")
    {
        using expected = __int(0);
    };

    TEST("test 2")
    {
        using expected = __sub(__int(1), __int(1));
    };
};

如上TEARDOWN的定義一般在fixture中所有測(cè)試用例的前面,但執(zhí)行卻在每個(gè)測(cè)試用例的后面。要如何實(shí)現(xiàn)TEARDOWN呢?借助前面實(shí)現(xiàn)setup的經(jīng)驗(yàn),似乎我們需要用戶通過TEARDOWN定義一個(gè)類,每個(gè)測(cè)試用例能夠繼承它,然后再將自身傳遞給它。

于是我們想到CRTP(Curiously Recurring Template Pattern)模式,它的用法如下:

template<TestCase>
struct TlpTestTeardown
{
    // 可以在此處使用TestCase類型的內(nèi)部定義
};

struct TlpTestCase : TlpTestTearDown<TlpTestCase>
{
    // ...
};

可以看到CRTP允許我們先定義一個(gè)父類模板,然后子類繼承這個(gè)模板的時(shí)候把自身當(dāng)做模板參數(shù)再傳遞給父類模板,于是父類模板中就可以使用子類類型了。

問題是對(duì)于完成teardown功能的父類模板到底如何使用作為每個(gè)測(cè)試用例的子類類型?最直接的想法是讓TlpTestTeardown再繼承自Test,這樣每個(gè)測(cè)試用例中的內(nèi)容就對(duì)TlpTestTeardown直接可見了。

template<TestCase>
struct TlpTestTeardown : TestCase
{
    // ...
};

struct TlpTestCase : TlpTestTearDown<TlpTestCase>
{
    // ...
};

遺憾的是,上述代碼并不能編譯通過。原因是TlpTestTeardown<TlpTestCase>將TlpTestCase作為父類,所以編譯器編譯到這句的時(shí)候需要TlpTestCase的完整定義,而此時(shí)TlpTestCase才開始定義。

這警告了我們,如果TlpTestTearDown繼承了TlpTestCase,就得在TlpTestTearDown<TlpTestCase>時(shí)刻看到TlpTestCase的完整定義。那么我們是否可以將TlpTestTearDown<TlpTestCase>挪到TlpTestCase定義的后面。

struct TlpTestCase
{
    // ...
};

struct : TlpTestTearDown<TlpTestCase>{};

這可以通過編譯,但是從此以后所有的測(cè)試用例后面都得加上struct : TlpTestTearDown<TlpTestCase>{}。這意味著我們需要給宏TEST實(shí)現(xiàn)一個(gè)對(duì)應(yīng)的結(jié)束宏,專門用來隱藏struct : TlpTestTearDown<TlpTestCase>{}。這導(dǎo)致我們以后定義測(cè)試用例不能再使用常用的花括號(hào),而是要像下面這樣:

TEST_BEGIN("test description")
    // 測(cè)試用例內(nèi)容實(shí)現(xiàn)于此
TEST_END() // 用于隱藏“}; struct : TlpTestTearDown<TlpTestCase>{};”

這很討厭!由于定義測(cè)試用例是如此的頻繁,我們還是希望保持之前那種簡(jiǎn)潔的定義方式!

我們?cè)倩氐?code>struct TlpTestCase : TlpTestTearDown<TlpTestCase>寫法中,看來將這種組合關(guān)系的定義放在花括號(hào)的前面就處理完畢是必要的。那么還有辦法嗎?我們想起,如果在TlpTestTearDown的成員方法中使用TlpTestCase類型,那么可以延遲對(duì)TlpTestCase完整定義的依賴時(shí)刻。于是我們將TlpTestTearDown的實(shí)現(xiàn)修改如下:

template<typename TestCase>  struct TlpTestTeardown
{
    TlpTestTeardown()
    {
        struct Teardown : TestCase
        {
            // 在這里實(shí)現(xiàn)teardown的定義,這里可以使用TestCase的內(nèi)部定義!
        };
    }
};

在上面的代碼中,我們?cè)?code>TlpTestTeardown模板的構(gòu)造函數(shù)里面定義了一個(gè)臨時(shí)類Teardown繼承了模板的入?yún)estCase。在該臨時(shí)類里面我們可以定義teardown的具體內(nèi)容,它可以訪問TestCase類的內(nèi)部定義。由于現(xiàn)在該臨時(shí)類定義在TlpTestTeardown的成員函數(shù)里,所以struct TlpTestCase : TlpTestTearDown<TlpTestCase>時(shí)刻并不需要TlpTestCase的完整定義,編譯器會(huì)將TlpTestTearDown的構(gòu)造函數(shù)的實(shí)例化放到當(dāng)前cpp文件的最后,這時(shí)已經(jīng)能夠看到TlpTestCase的完整定義了。

當(dāng)使用上面這種技術(shù)之后,TEST宏定義修改如下:

#define TEST(name) struct UNIQUE_NAME(tlp_test_) :TlpTestSetup, TlpTestTearDown<UNIQUE_NAME(tlp_test_)>

于是定義測(cè)試用例的用法還和以前一樣,仍舊可以使用熟悉的花括號(hào)。然而teardown的定義卻會(huì)稍微的不統(tǒng)一:

#define TEARDOWN_BEGIN()                            \
template<typename TestCase>  struct TlpTestTeardown \
{                                                   \
    TlpTestTeardown()                               \
    {                                               \
        struct Teardown : TestCase                  \
        {

#define TEARDOWN_END()  }; } };

可以看到對(duì)于teardown的定義將不能像setup和testcase那樣使用花括號(hào)了,而是使用TEARDOWN_BEGIN()TEARDOWN_END()。雖然風(fēng)格有所差別,但這已經(jīng)是我能想到的最好辦法了。

為了讓setup的定義能夠和teardown統(tǒng)一,我們也給setup的定義提供了一套對(duì)稱的宏定義SETUP_BEGIN()SETUP_END()。當(dāng)fixture中需要同時(shí)出現(xiàn)setup和teardown時(shí),setup可以使用和teardown一致的風(fēng)格去定義。其它情況下,setup還是使用花括號(hào)的風(fēng)格。

上述技術(shù)中其實(shí)還存在一個(gè)問題,TlpTestTeardown構(gòu)造函數(shù)中Teardown類繼承的TestCase是從模板參數(shù)輸入的,所以在Teardown中是不能直接引用TestCase中的定義的,必須加上TestCase::前綴。為此我們實(shí)現(xiàn)了一個(gè)更有語(yǔ)義的宏__test_refer(),用于顯示指定teardown中某一個(gè)變量引用自TestCase。

#define __test_refer(...)       typename TestCase::__VA_ARGS__

最終,我們可以如下定義一個(gè)完整的fixture:

FIXTURE(TestWithSetUpAndTearDown)
{
    SETUP_BEGIN()
        using Expected = __int(2);
    SETUP_END()

    TEARDOWN_BEGIN()
        ASSERT_EQ(__test_refer(Result), Expected);
    TEARDOWN_END()

    TEST("test1")
    {
        using Result = __add(__int(1), __int(1));
    };

    TEST("test2")
    {
        using Result = __div(__int(4), __int(2));
    };
};

我們不得不承認(rèn),引入teardown后讓整個(gè)框架復(fù)雜了很多,而且風(fēng)格也趨向不一致。但好在對(duì)于具有不可變性的元編程來說,清理測(cè)試上下文的事情本就意義不大,所以一般在fixture中需要定義teardown的機(jī)會(huì)并不多。關(guān)鍵地我們沒有因?yàn)橐雝eardown的功能損害到原有定義testcase的簡(jiǎn)潔性。而且在沒有teardown的fixture中,setup仍舊可以像以前一樣使用花括號(hào)。所以絕大多數(shù)情況下,還是可以接受的!

不過我相信應(yīng)該還有更好的實(shí)現(xiàn)方式,不過限于本人能力,只能到此!如果你了解更好的實(shí)現(xiàn)方式,還請(qǐng)不吝賜教!


測(cè)試報(bào)告

返回 C++11模板元編程 - 目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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