C++ 的 gmock 小技巧

在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);
}

將程式碼的順序反過來寫,卻可以保有同樣的執行順序,就是我想達成的目的,有幾種方法

  1. 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());
  }
}
  1. 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());
  }

  1. 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());
  });
}