在C++寫測試時可能會用到gmock來輔助,相對於測試的程式碼,是先執行動作後,才ASSERT回傳的結果,
mock通常是先EXPECT接下來會發生的事,才執行動作。
這樣在閱讀程式碼的時候就需要跳著看:先看要執行的動作是什麼,再看做了哪些假設。
如下是一個簡單的例子:
TEST(ShopTest, Normal)
{
NaggyMock<MockRepo> repo;
Shop shop(&repo);
EXPECT_CALL(repo, AddItem(87, _)).WillOnce(Return());
EXPECT_CALL(repo, Equip(87, _)).WillOnce(Return());
shop.InitUser(87);
EXPECT_CALL(repo, AddItem(56, _)).WillOnce(Return());
EXPECT_CALL(repo, Equip(56, _)).WillOnce(Return());
shop.InitUser(56);
}
將程式碼的順序反過來寫,卻可以保有同樣的執行順序,就是我想達成的目的,有幾種方法
- shared_ptr 的 deleter
這類似自行撰寫一個class在dtor呼叫特定function,並傳入lambda來指定行為。
這邊的...
是不定長度參數,用來省略shared_ptr會傳入的參數。
相對的問題是需要指定一個scope控制defer object的生命週期,造成程式碼多出不必要的行數。
3 lines -> 6 lines
using defer = shared_ptr<void>;
TEST(ShopTest, SharedPointerDefer)
{
NaggyMock<MockRepo> repo;
Shop shop(&repo);
{
defer __(nullptr, [&](...) { shop.InitUser(87); });
EXPECT_CALL(repo, AddItem(87, _)).WillOnce(Return());
EXPECT_CALL(repo, Equip(87, _)).WillOnce(Return());
}
{
defer __(nullptr, [&](...) { shop.InitUser(56); });
EXPECT_CALL(repo, AddItem(56, _)).WillOnce(Return());
EXPECT_CALL(repo, Equip(56, _)).WillOnce(Return());
}
}
- BOOST_SCOPE_EXIT
效果跟1一樣,但較為簡潔,不需要自己寫nullptr…
但clang-format的特定選項會使其展開,而生出更多無謂的行數。
3 lines -> 6 lines -> 9 lines
{
BOOST_SCOPE_EXIT_ALL(&)
{
shop.InitUser(87);
};
EXPECT_CALL(repo, AddItem(87, _)).WillOnce(Return());
EXPECT_CALL(repo, Equip(87, _)).WillOnce(Return());
}
- C Macro
利用c的macro交換statment的執行順序。
這邊因為只提供兩個參數,在EXPECT有多行時需要自己用{}
包起來。
更進階的作法是利用__VA_ARGS__
提供不定長度參數的macro並展開。
#define WHEN(x, y) \
if(1) { \
y; \
x; \
}
TEST(ShopTest, CMacro)
{
NaggyMock<MockRepo> repo;
Shop shop(&repo);
WHEN(shop.InitUser(87), {
EXPECT_CALL(repo, AddItem(87, _)).WillOnce(Return());
EXPECT_CALL(repo, Equip(87, _)).WillOnce(Return());
});
WHEN(shop.InitUser(56), {
EXPECT_CALL(repo, AddItem(56, _)).WillOnce(Return());
EXPECT_CALL(repo, Equip(56, _)).WillOnce(Return());
});
}