C++ 的單元測試工具 —— Catch

[原文地址:http://blog.guorongfei.com/2016/08/22/cpp-unit-test-catch/]
如果你平常使用 Java 語言做開發(fā)碌秸,當你聽到單元測試工具的時候吼具,你很可能馬上會想起 JUnit渤早。作為一名C++軟件工程師谤专,當我第一次打算給我的程序做單元測試的時候训措,我的第一 想法是:有這樣的工具嗎郑临?經(jīng)過一段時間的搜索之后颜骤,我的反應變成了:我該用哪一個聂渊?

我在學校的時候孝宗,很少聽說C++的單元測試工具穷躁,以至于我一直認為這樣的工具是不存在。 后來慢慢的發(fā)現(xiàn)我們可以選擇的遠比你想象中的要多得多:Catch, Boost.Test, UnitTest++, lest, bandit, igloo, xUnit++, CppTest, CppUnit, CxxTest, cpputest, googletest, cute因妇。

那我們應該使用哪一個呢问潭?如果你在 Google 里面搜索:best c++ unit testing framework。頭兩條婚被,一條是 stackoverflow 的問答狡忙,另一條是 reddit 的問答 。這兩個問題都指向同一個單元測試框架:Catch址芯。

為什么使用 Catch

在 Catch 的官方文檔中有一篇:Why do we need yet another C++ test framework? 有興趣的可以去看看灾茁。對我來說,它最吸引我的地方主要是:

  • 幾乎不用配置谷炸,它是一個單頭文件的測試框架北专,壓根不要什么<header class="post-header" style="opacity: 1; display: block; transform: translateY(0px);">

C++ 的單元測試工具 —— Catch

發(fā)表于 <time title="創(chuàng)建于" itemprop="dateCreated datePublished" datetime="2016-08-22T15:37:30+08:00">2016-08-22</time> | 閱讀次數(shù): 2356

</header>

如果你平常使用 Java 語言做開發(fā),當你聽到單元測試工具的時候,你很可能馬上會想起 JUnit。作為一名C++軟件工程師借跪,當我第一次打算給我的程序做單元測試的時候,我的第一 想法是:有這樣的工具嗎驶睦?經(jīng)過一段時間的搜索之后,我的反應變成了:我該用哪一個匿醒?

我在學校的時候场航,很少聽說C++的單元測試工具,以至于我一直認為這樣的工具是不存在青抛。 后來慢慢的發(fā)現(xiàn)我們可以選擇的遠比你想象中的要多得多:Catch, Boost.Test, UnitTest++, lest, bandit, igloo, xUnit++, CppTest, CppUnit, CxxTest, cpputest, googletest, cute旗闽。

那我們應該使用哪一個呢?如果你在 Google 里面搜索:best c++ unit testing framework。頭兩條适室,一條是 stackoverflow 的問答嫡意,另一條是 reddit 的問答 。這兩個問題都指向同一個單元測試框架:Catch捣辆。

為什么使用 Catch

在 Catch 的官方文檔中有一篇:Why do we need yet another C++ test framework? 有興趣的可以去看看蔬螟。對我來說,它最吸引我的地方主要是:

  • 幾乎不用配置汽畴,它是一個單頭文件的測試框架旧巾,壓根不要什么額外的配置就可以使用
  • 語法非常簡單明了,用它寫的測試代碼和自然語言一樣易懂忍些。

如何使用它

Catch 是單頭文件庫鲁猩,你直接 #include “catch.hpp” 它就可以了。然后你就可以像下面 這樣寫測試代碼:

|

<pre>SCENARIO( "vectors can be sized and resized", "[vector]" ) {

GIVEN( "A vector with some items" ) {
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

WHEN( "the size is increased" ) {
v.resize( 10 );

THEN( "the size and capacity change" ) {
REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
}
}
WHEN( "the size is reduced" ) {
v.resize( 0 );

THEN( "the size changes but not capacity" ) {
REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
}
}
WHEN( "more capacity is reserved" ) {
v.reserve( 10 );

THEN( "the capacity changes but not the size" ) {
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
}
WHEN( "less capacity is reserved" ) {
v.reserve( 0 );

THEN( "neither size nor capacity are changed" ) {
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
}
}
}
}
</pre>

|

這幾乎是不需要解釋就可以理解的讀懂的代碼罢坝。這種測試方式稱為 BDD(Behaviour Driven Development)廓握,是最新的一種測試方式,它強調(diào)的是“行為”而不是“測試”嘁酿,有興趣可以看 看這篇文章隙券。

如果你習慣傳統(tǒng)的TDD測試,你可以像下面這樣寫測試代碼:

|

<pre>TEST_CASE( "vectors can be sized and resized", "[vector]" ) {

std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

SECTION( "resizing bigger changes size and capacity" ) {
v.resize( 10 );

REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
}
SECTION( "resizing smaller changes size but not capacity" ) {
v.resize( 0 );

REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
}
SECTION( "reserving bigger changes capacity but not size" ) {
v.reserve( 10 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
SECTION( "reserving smaller does not change size or capacity" ) {
v.reserve( 0 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
}
}
</pre>

|

實際上這兩種方式是等價闹司,SCENARIO 只是 TEST_CASE 的別名娱仔,GIVEN、WHEN游桩、THEN 最終 也是 map 到 SECTION 上面的牲迫。這其中的差異只是存在于測試的思維不同而已,你完全可以 根據(jù)自己的喜好使用你最喜歡的方式即可众弓。

SECTION 的執(zhí)行順序

上面的代碼很清晰易懂恩溅,不過有一個地方需要注意隔箍,那就是 SECTION 的執(zhí)行方式谓娃。在上一 小節(jié)的代碼中,TEST_CASE 中有 4 個 SECTION蜒滩,它們并不是單純的順序執(zhí)行關(guān)系滨达。在第 一個 SECTION 執(zhí)行完成之后,會重頭開始執(zhí)行并跳過已經(jīng)執(zhí)行過的 SECTION俯艰。也就是說上 面的代碼的執(zhí)行路徑大概是這樣的(去掉了 SECTION 宏之后):

|

<pre>// SECTION 1
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

v.resize( 10 );

REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );

// SECTION 2
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

v.resize( 0 );

REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );

// SECTION 3
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

v.reserve( 10 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );

// SECTION 4
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

v.reserve( 0 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
</pre>

|

測試代碼的執(zhí)行入口

在C++中任何代碼需要執(zhí)行捡遍,都需要通過 main 函數(shù)這個入口,測試代碼也不例外竹握。Catch 不需要我們自己編寫 main 函數(shù)去調(diào)用這些測試代碼画株。它提供了默認的 main 函數(shù)入口,你 只需要在(而且僅在)一個文件中加入下面的配置宏:

|

<pre>#define CATCH_CONFIG_MAIN

include "catch.hpp"

</pre>

|

最佳實踐

最佳實踐是單獨用一個文件放這兩行代碼,把測試代碼寫在其他的文件中谓传。

之所以這樣做是因為Catch是單頭文件庫,這意味著它里面的內(nèi)容會最終出現(xiàn)在所有的包含 這個頭文件的編譯單元中续挟。如果我們把測試代碼和上面兩行代碼放在一起會導致每次編譯測 試代碼的時候都需要編譯 Catch 的內(nèi)核紧卒,這會導致編譯速度非常非常的慢。如果把兩者分 開诗祸,Catch 的內(nèi)核只需要在一個文件中編譯一次(因為 Catch 內(nèi)部做了判斷跑芳,如果內(nèi)核編 譯過了是不需要再次編譯的,即使你在多個文件中使用了 #include “catch.hpp”)直颅。這個 文件的編譯速度相對較慢博个,但是這個文件不會改動所以整個開發(fā)周期中它只需要編譯一次, 而不斷更新的測試代碼的編譯速度會因此快很多功偿。

命令行參數(shù)

Catch 提供的這個 main 函數(shù)實現(xiàn)的另一個強大的功能是豐富的命令行參數(shù)坡倔,你可以選擇執(zhí) 行其中的某些 TEST_CASE,也可以選擇不執(zhí)行其中的某些 TEST_CASE脖含,你可以用它調(diào)整 輸出到 xml 文件罪塔,也可以用它從文件中讀取需要測試的用例。這些命令的具體使用請參考 Catch 的官方文檔Command line一節(jié)养葵。

TAG

需要注意的是征堪,這些強大的命令行大多數(shù)是基于 TAG 的,也就是 TEST_CASE 定義中的第 二個參數(shù)关拒。

|

<pre>TEST_CASE( "vectors can be sized and resized", "[vector]" )
</pre>

|

上面的定義中 “[vector]” 就是一個 TAG佃蚜,你可以提供多個 TAG:

|

<pre>TEST_CASE( "D", "[widget][gadget]" ) { /* ... */ }
</pre>

|

這樣的話你可以在命令行中根據(jù) TAG 去選擇是否需要執(zhí)行該 TEST_CASE。比如:

|

<pre>./catch "[vector]" // 只執(zhí)行那些標記為 vector 的測試用例
</pre>

|

此外你還可以使用一些特殊的字符着绊,比如 [.] 表示隱藏谐算。[.integration] 則表示默認 隱藏,但是可以在命令行中使用 [.integration] 這個 TAG 執(zhí)行归露。其他的一些特殊的字符 請參考官方文檔的Test cases and sections一節(jié)

|

<pre>./catch // 默認不執(zhí)行 integration

./catch "[.integration]" // 使用 TAG 執(zhí)行 integration
</pre>

|

提供自己的 main 函數(shù)入口

如果你不喜歡上面的處理方式洲脂,想要自己提供 main 函數(shù),你可以使用 CATCH_CONFIG_RUNNER剧包,具體的細節(jié)請查看官方文檔中的 Supplying main() yourself一節(jié)恐锦。

其他內(nèi)容

其實 Catch 本身相對來說比較簡單,不需要太多其他的學習疆液,大部分的用法是非常的直觀 的一铅,看完它的官方教程之后基本上可以上手了,然后有時間慢慢的讀一讀它的官方文 檔集合
額外的配置就可以使用

  • 語法非常簡單明了堕油,用它寫的測試代碼和自然語言一樣易懂潘飘。

如何使用它

Catch 是單頭文件庫肮之,你直接 #include “catch.hpp” 它就可以了。然后你就可以像下面 這樣寫測試代碼:

|

<pre>SCENARIO( "vectors can be sized and resized", "[vector]" ) {

GIVEN( "A vector with some items" ) {
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

WHEN( "the size is increased" ) {
v.resize( 10 );

THEN( "the size and capacity change" ) {
REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
}
}
WHEN( "the size is reduced" ) {
v.resize( 0 );

THEN( "the size changes but not capacity" ) {
REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
}
}
WHEN( "more capacity is reserved" ) {
v.reserve( 10 );

THEN( "the capacity changes but not the size" ) {
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
}
WHEN( "less capacity is reserved" ) {
v.reserve( 0 );

THEN( "neither size nor capacity are changed" ) {
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
}
}
}
}
</pre>

|

這幾乎是不需要解釋就可以理解的讀懂的代碼卜录。這種測試方式稱為 BDD(Behaviour Driven Development)局骤,是最新的一種測試方式,它強調(diào)的是“行為”而不是“測試”暴凑,有興趣可以看 看這篇文章峦甩。

如果你習慣傳統(tǒng)的TDD測試,你可以像下面這樣寫測試代碼:

|

<pre>TEST_CASE( "vectors can be sized and resized", "[vector]" ) {

std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

SECTION( "resizing bigger changes size and capacity" ) {
v.resize( 10 );

REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
}
SECTION( "resizing smaller changes size but not capacity" ) {
v.resize( 0 );

REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
}
SECTION( "reserving bigger changes capacity but not size" ) {
v.reserve( 10 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
SECTION( "reserving smaller does not change size or capacity" ) {
v.reserve( 0 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
}
}
</pre>

|

實際上這兩種方式是等價现喳,SCENARIO 只是 TEST_CASE 的別名凯傲,GIVEN、WHEN嗦篱、THEN 最終 也是 map 到 SECTION 上面的冰单。這其中的差異只是存在于測試的思維不同而已,你完全可以 根據(jù)自己的喜好使用你最喜歡的方式即可灸促。

SECTION 的執(zhí)行順序

上面的代碼很清晰易懂诫欠,不過有一個地方需要注意,那就是 SECTION 的執(zhí)行方式浴栽。在上一 小節(jié)的代碼中荒叼,TEST_CASE 中有 4 個 SECTION,它們并不是單純的順序執(zhí)行關(guān)系典鸡。在第 一個 SECTION 執(zhí)行完成之后被廓,會重頭開始執(zhí)行并跳過已經(jīng)執(zhí)行過的 SECTION。也就是說上 面的代碼的執(zhí)行路徑大概是這樣的(去掉了 SECTION 宏之后):

|

<pre>// SECTION 1
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

v.resize( 10 );

REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );

// SECTION 2
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

v.resize( 0 );

REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );

// SECTION 3
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

v.reserve( 10 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );

// SECTION 4
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

v.reserve( 0 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
</pre>

|

測試代碼的執(zhí)行入口

在C++中任何代碼需要執(zhí)行萝玷,都需要通過 main 函數(shù)這個入口嫁乘,測試代碼也不例外。Catch 不需要我們自己編寫 main 函數(shù)去調(diào)用這些測試代碼球碉。它提供了默認的 main 函數(shù)入口蜓斧,你 只需要在(而且僅在)一個文件中加入下面的配置宏:

|

<pre>#define CATCH_CONFIG_MAIN

include "catch.hpp"

</pre>

|

最佳實踐

最佳實踐是單獨用一個文件放這兩行代碼,把測試代碼寫在其他的文件中睁冬。

之所以這樣做是因為Catch是單頭文件庫挎春,這意味著它里面的內(nèi)容會最終出現(xiàn)在所有的包含 這個頭文件的編譯單元中。如果我們把測試代碼和上面兩行代碼放在一起會導致每次編譯測 試代碼的時候都需要編譯 Catch 的內(nèi)核痴突,這會導致編譯速度非常非常的慢搂蜓。如果把兩者分 開狼荞,Catch 的內(nèi)核只需要在一個文件中編譯一次(因為 Catch 內(nèi)部做了判斷辽装,如果內(nèi)核編 譯過了是不需要再次編譯的,即使你在多個文件中使用了 #include “catch.hpp”)相味。這個 文件的編譯速度相對較慢拾积,但是這個文件不會改動所以整個開發(fā)周期中它只需要編譯一次, 而不斷更新的測試代碼的編譯速度會因此快很多。

命令行參數(shù)

Catch 提供的這個 main 函數(shù)實現(xiàn)的另一個強大的功能是豐富的命令行參數(shù)拓巧,你可以選擇執(zhí) 行其中的某些 TEST_CASE斯碌,也可以選擇不執(zhí)行其中的某些 TEST_CASE,你可以用它調(diào)整 輸出到 xml 文件肛度,也可以用它從文件中讀取需要測試的用例傻唾。這些命令的具體使用請參考 Catch 的官方文檔Command line一節(jié)。

TAG

需要注意的是承耿,這些強大的命令行大多數(shù)是基于 TAG 的冠骄,也就是 TEST_CASE 定義中的第 二個參數(shù)。

|

<pre>TEST_CASE( "vectors can be sized and resized", "[vector]" )
</pre>

|

上面的定義中 “[vector]” 就是一個 TAG加袋,你可以提供多個 TAG:

|

<pre>TEST_CASE( "D", "[widget][gadget]" ) { /* ... */ }
</pre>

|

這樣的話你可以在命令行中根據(jù) TAG 去選擇是否需要執(zhí)行該 TEST_CASE凛辣。比如:

|

<pre>./catch "[vector]" // 只執(zhí)行那些標記為 vector 的測試用例
</pre>

|

此外你還可以使用一些特殊的字符,比如 [.] 表示隱藏职烧。[.integration] 則表示默認 隱藏扁誓,但是可以在命令行中使用 [.integration] 這個 TAG 執(zhí)行。其他的一些特殊的字符 請參考官方文檔的Test cases and sections一節(jié)

|

<pre>./catch // 默認不執(zhí)行 integration

./catch "[.integration]" // 使用 TAG 執(zhí)行 integration
</pre>

|

提供自己的 main 函數(shù)入口

如果你不喜歡上面的處理方式蚀之,想要自己提供 main 函數(shù)蝗敢,你可以使用 CATCH_CONFIG_RUNNER,具體的細節(jié)請查看官方文檔中的 Supplying main() yourself一節(jié)足删。

其他內(nèi)容

其實 Catch 本身相對來說比較簡單前普,不需要太多其他的學習,大部分的用法是非常的直觀 的壹堰,看完它的官方教程之后基本上可以上手了拭卿,然后有時間慢慢的讀一讀它的官方文 檔集合

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市贱纠,隨后出現(xiàn)的幾起案子峻厚,更是在濱河造成了極大的恐慌,老刑警劉巖谆焊,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惠桃,死亡現(xiàn)場離奇詭異辖试,居然都是意外死亡辜王,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門罐孝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呐馆,“玉大人,你說我怎么就攤上這事莲兢⌒诶矗” “怎么了续膳?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長收班。 經(jīng)常有香客問我坟岔,道長,這世上最難降的妖魔是什么摔桦? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任社付,我火速辦了婚禮,結(jié)果婚禮上邻耕,老公的妹妹穿的比我還像新娘瘦穆。我一直安慰自己,他們只是感情好赊豌,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布扛或。 她就那樣靜靜地躺著,像睡著了一般碘饼。 火紅的嫁衣襯著肌膚如雪熙兔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天艾恼,我揣著相機與錄音住涉,去河邊找鬼。 笑死钠绍,一個胖子當著我的面吹牛舆声,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柳爽,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼媳握,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了磷脯?” 一聲冷哼從身側(cè)響起蛾找,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赵誓,沒想到半個月后打毛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡俩功,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年幻枉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诡蜓。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡熬甫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出万牺,到底是詐尸還是另有隱情罗珍,我是刑警寧澤洽腺,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布脚粟,位于F島的核電站覆旱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏核无。R本人自食惡果不足惜扣唱,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望团南。 院中可真熱鬧噪沙,春花似錦、人聲如沸吐根。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拷橘。三九已至局义,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冗疮,已是汗流浹背萄唇。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留术幔,地道東北人另萤。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像诅挑,于是被迫代替她去往敵國和親四敞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354