Google Test を使ってみる(その3:テストフィクスチャ編)

| 6 min read
Author: shuichi-takatsu shuichi-takatsuの画像

前回は簡単なサンプルプログラムのテストケースを「Google Test」を使って記述し、テストを実行してみました。
今回はもう少し複雑なプログラムのテストケースを Google Test の「テストフィクスチャ」を使って動かしてみます。

テストフィクスチャ とは

#

複数のテストケースを書いていると、複数のテストケースに同じセットアップ(テストケースに事前に値を設定するなど)を使いまわしたい場合があります。
データの”使いまわし(再利用)”を簡単に実現するには「グローバル変数」を用いるのが最も簡単ですが、グローバル変数は名前空間を汚染するだけでなく、うっかり初期化や終了処理を怠ると他のテストケースに影響が伝搬してしまいます。

そのような場合には「テストフィクスチャ」を使用します。
テストフィクスチャを使うと、テストケース毎に毎回新しい変数(データ)が利用でき、テストの独立性が保たれます。

テストフィクスチャはクラスとして実装します。
Google Test にはテストフィクスチャの基底クラスが用意されているので、利用者はこの基底クラスを継承したテストフィクスチャを作成し、必要なデータを設定することができます。

お題:カウンタクラスの実装

#

Google Test が提供するサンプルプログラムにも同様の機能を持つクラスが実装されていますが、今回はもっと簡略化した「加算と減算のメソッドを持つクラス(カウンタクラス)」を作成し、このカウンタクラスのテストを通して、テストフィクスチャの動作を確認していきます。
カウンタクラスの加算・減算メソッドの戻り値は正の整数とゼロのみとします。

サンプルのカウンタクラスのソースコードを以下に示します。

カウンタクラス:counter.h, counter.cc

counter.h

#ifndef _COUNTER_H_
#define _COUNTER_H_

class Counter {
private:
int counter_;

public:
Counter() {} // 何もしない
~Counter() {} // 何もしない

void Init(); // 初期化
int Increment(); // 加算
int Decrement(); // 減算
};

#endif // _COUNTER_H_

counter.cc

#include "counter.h"

void Counter::Init() {
counter_ = 0; // 初期化
}

int Counter::Increment() {
return ++counter_;
}

int Counter::Decrement() {
if (counter_ <= 0) {
return counter_;
} else {
return --counter_;
}
}

カウンタクラスのソースコードの解説

#

カウンタクラスには「Init」「Increment」「Decrement」の3つのメソッドが用意されています。
コンストラクタ、デストラクタは何もしていません。
Init は内部変数「counter_」の値をゼロに初期化します。
「初期化だけならコンストラクタの初期化子で実装すればいいじゃん」と言われそうですが、今回はわざと「Init」を呼び出さないと内部変数の初期化がされない状態を作り出しています。
Increment は内部変数をプラス1した後の変数の値を戻り値にします。
Decrement は内部変数がゼロ以下の場合、そのままの値を戻り値にし、ゼロより大きい場合はマイナス1した後の変数の値を戻り値にします。(今回の場合、内部変数の値が0より小さい値になることはありませんが、筆者はこのように実装する癖が付いています。)

テストフィクスチャを使わずに、カウンタクラスのテストケース(Test1)を書く

#

今回4つのテストケースを用意します。

  • 加算を繰り返すテスト(テスト名:Increment)
  • 減算を繰り返すテスト(テスト名:Decrement)
  • 加算と減算を複数回繰り返すテスト(テスト名:Both)
  • 減算してもマイナスにならないことの確認テスト(テスト名:Error)

counter_test1.cc

#include <gtest/gtest.h>

#include "counter.h"

namespace {

TEST(CounterTest, Increment) {
// クラスを用意して、初期化する
Counter c;
c.Init();

EXPECT_EQ(1, c.Increment()); // 期待値:1
EXPECT_EQ(2, c.Increment()); // 期待値:2
}

TEST(CounterTest, Decrement) {
// クラスを用意して、初期化する
Counter c;
c.Init();

EXPECT_EQ(0, c.Decrement()); // 期待値:0
EXPECT_EQ(0, c.Decrement()); // 期待値:0
}

TEST(CounterTest, Both) {
// クラスを用意して、初期化する
Counter c;
c.Init();

EXPECT_EQ(1, c.Increment()); // 期待値:1
EXPECT_EQ(2, c.Increment()); // 期待値:2

EXPECT_EQ(1, c.Decrement()); // 期待値:1
EXPECT_EQ(0, c.Decrement()); // 期待値:0

EXPECT_EQ(1, c.Increment()); // 期待値:1
}

TEST(CounterTest, Error) {
// クラスを用意して、初期化する
Counter c;
c.Init();

EXPECT_NE(-1, c.Decrement()); // 期待値:0
}

} // namespace

テストケース(Test1)の実行

#

コマンドラインでテストをコンパイルします。

g++ counter.cc counter_test1.cc -o counter1 -g -pthread -lgtest_main -lgtest

テストを実行した結果を以下に示します。

期待した通りの結果になりました。
テストとしては結果的に目的を達成しているわけですが、テストケースに毎回

  // クラスを用意して、初期化する
Counter c;
c.Init();

を記述しなくてはなりません。
もっとテストケースを”スマートに”書きたいと思います。
テストケース内にはテストに必要な処理以外は極力書きたくないのです。

テストフィクスチャを使って、カウンタクラスのテストケース(Test2)を書く

#

テストフィクスチャは

testing::Test

を継承して作成します。
今回の場合、テストフィクスチャを以下のように実装しました。

// フィクスチャクラスを作る
class CounterTest : public testing::Test {
protected:
Counter c;

virtual void SetUp() {
c.Init();
}
// virtual void TearDown() {}
};

今回作成したカウンタクラスには終了処理は必要無いので、初期化セットアップの部分のみを記述しています。
その部分が

    virtual void SetUp() {
c.Init();
}

になります。
この部分でカウンタクラスの内部変数を初期化する「Init」メソッドを呼び出しています。

また、テストケースから参照される内部変数「c」は protected で定義します。

  protected:
Counter c;

テストフィクスチャを使ったテストケース全体を以下のようにしました。

counter_test2.cc

#include <gtest/gtest.h>

#include "counter.h"

namespace {

// フィクスチャークラスを作る
class CounterTest : public testing::Test {
protected:
Counter c;

virtual void SetUp() {
c.Init();
}
// virtual void TearDown() {}
};

TEST_F(CounterTest, Increment) {
// Counter c; としても良いが、フィクスチャーとしてデータやクラスを使いまわしたいので、CounterTestというフィクスチャークラスを作る

EXPECT_EQ(1, c.Increment()); // 期待値:1
EXPECT_EQ(2, c.Increment()); // 期待値:2
}

TEST_F(CounterTest, Decrement) {
EXPECT_EQ(0, c.Decrement()); // 期待値:0
EXPECT_EQ(0, c.Decrement()); // 期待値:0
}

TEST_F(CounterTest, Both) {
EXPECT_EQ(1, c.Increment()); // 期待値:1
EXPECT_EQ(2, c.Increment()); // 期待値:2

EXPECT_EQ(1, c.Decrement()); // 期待値:1
EXPECT_EQ(0, c.Decrement()); // 期待値:0

EXPECT_EQ(1, c.Increment()); // 期待値:1
}

TEST_F(CounterTest, Error) {
EXPECT_NE(-1, c.Decrement()); // 期待値:0
}

} // namespace

先ほどの「counter_test1.cc」と異なるところは以下の点です。

  • TEST マクロから TEST_F マクロに変更されている
  • 各テストケース内から、counter_test1では必要だった「カウンタクラス実体登録」と「初期化メソッド呼び出し」の処理が無くなっている
  • テストケース名の部分にテストフィクスチャクラスそのもの(CounterTest)を渡している

テストケース内で参照しているカウンタクラスの実体「c」は特別な設定をすることなく、そのままテストケース内で参照が可能になっています。

テストケース(Test2)の実行

#

コマンドラインでテストをコンパイルします。

g++ counter.cc counter_test2.cc -o counter2 -g -pthread -lgtest_main -lgtest

テストを実行した結果を以下に示します。

Test1の場合と同様の結果になりました。
正しく「テストフィクスチャ」が機能していることがわかりました。

おまけ

#

もし、Initメソッドを呼ばずにテストを実行した場合はどうなるでしょうか?
試しにInitメソッドを呼ばないバージョンを作成し、Incrementテストを実行してみます。
結果は以下のようになりました。

クラスの初期化が実行されていないため、テストが失敗していることがわかります。

まとめ

#

今回は Google Test のテストフィクスチャを使うことで、テストケースを実行する前に、データをテストケースに渡すことができることを確認しました。
次回はテストケースの実行をサポートするVSCode拡張機能「GoogleTest Adapter」の設定や使い方を紹介したいと思います。

ソフトウェアテストに関する技法やテクニックをまとめています。

テストに活用していただければ幸いです。

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. 基本から理解するJWTとJWT認証の仕組み (2022-12-08)
  2. AWS認定資格を12個すべて取得したので勉強したことなどをまとめます (2022-12-12)
  3. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  4. Viteベースの高速テスティングフレームワークVitestを使ってみる (2022-12-28)
  5. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  6. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  7. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  8. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  9. GitHub Actions - 構成変数(環境変数)が外部設定できるようになったので用途を整理する (2023-01-16)
  10. Nuxt3入門(第7回) - Nuxt3のプラグイン・ミドルウェアを使う (2022-10-23)