Travis CI上でGTestのvalue-parameterized testを実行する

こんにちは。先日Travis CI上でGoogle Test (GTest)のvalue-parameteraized testを実行する際、少し苦労したのでやり方をまとめておきます。

GTestのvalue-parameterized test


最近のC++のユニットテスト・フレームワークはいろいろあって、最近はCATCHとかいうのが良いよ、なんていわれたりしてますね。

それでも私がしぶとくGTestを使い続けているのはvalue-parameterized testがそれなりに使い易いからです。

value-parameterized testというのは、テストに渡す変数をパラメータ化して、テストに渡す変数を組み合わせにより自動生成したり、まとめて切り替えたり出来るようにするための仕組みです。

GTestのドキュメントだけだと正直やりかたが良く分からないと思うので、そこらへんも合わせて解説していきます。

フィクスチャ・クラスを使ったテスト


GTestではvalue-parameterized testはフィクスチャクラスを使って行います。ちなみにフィクスチャ・クラスというのは、テストの実行前の準備と後片付けをやるためのクラスだと思っていただければいいです。

JavaのJUnitとかPythonのunittestだとおなじみの機能だと思います。そこでまずは、フィクスチャ・クラスを使ったテストについて簡単に触れておきます。知ってる人は適当に読み飛ばしてください。

GTestでのフィクスチャクラスをつかったテストは次のようになります。

ここでは、普通の2次元の整数ポイントクラスをテストしています。このクラスのソースは下に載せて起きます。

#include "gtest/gtest.h"

#include "point.h"

class PointTest : public ::testing::Test {
protected:
    Point p0;

protected:
    PointTest() {}
    virtual ~PointTest() {}

    void SetUp() {
        p0 = Point(1, 2);
    }

    void TearDown() {
    }
};

TEST_F(PointTest, Copy) {
    Point p1(p0);
    EXPECT_EQ(p0.x(), p1.x());
    EXPECT_EQ(p0.y(), p1.y());
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

ソースの解説

このソースではフィクスチャ・クラスが各テストで共通に使う値p0を持っていて、それをSetUp関数の中で初期化しています。今回の場合は定数なのでstaticにしてもいいかもしれません。

実際のテストはフィクスチャクラスを使うことを明示してTEST**_F**というふうにFを付けてあげます。あとは通常のテスト実行と同じです。

Value-parameterized testのやり方


Value-parameterized testを扱うためにはフィクスチャ・クラスを拡張する必要があるのですが、拡張の仕方は2種類あります。

1つ目は先ほどのように::testing::Testクラスを敬称したフィクスチャ・クラスにたいして、WithParamInterfaceというテンプレート付きのインターフェースクラスを敬称する方法です。例えば次のような感じです。

 {
    // ...
};

このように宣言すると既存のフィクスチャ・クラスの準備や後片付けの機能を保ったまま、Pointクラスをパラメータとしたようなvalue-parameterized testを作成することができます。

2つ目のやり方は先ほどのPointTestのようなクラスにフィクスチャ・クラスとしての機能とvalue-parameterized testの機能を両方1度に付け加える方法です。

これにはTestWithParamというクラスを継承します。このクラスは::testing::TestとWithParamInterfaceの両方を継承したクラスになっており、次のような書き方で、先ほどと同じような機能を作ることができます。

 {
    // ...
};

このように定義したパラメータ付のフィクスチャ・クラスでテストをするにはTEST_Pというマクロを使います。基本はTEST_Fと使い方は同じなのですが、TEST_Pはその内部でGetParam()という関数をつかって、パラメータを受け取ることが出来ます。

例えば次のような感じです。

TEST_P(PointTestWithParam, Assign) {
    Point p1(p0);
    Point p2(GetParam());
    Point p3 = p1 + p2;
    EXPECT_EQ(p1.x() + p2.x(), p3.x());
    EXPECT_EQ(p1.y() + p2.y(), p3.y());
}

この例では、GetParamでパラメータとして入力されたPointを取得して、それをフィクスチャ・クラスで用意しておいたPointに加算しています。

この際のパラメータの渡し方ですが、GTestではパラメータを渡すためにINSTANTIATE_TEST_CASE_Pというマクロが用意されています。

このマクロの使い方は以下の通りです。

INSTANTIATE_TEST_CASE_P(, PointTestWithParam, ::testing::Values(Point(3, 4), Point(5, 6)));

マクロの第一引数はパラメータを管理するための名前が入力できるのですが、今回の例では省略しています。

第二引数はフィクスチャ・クラス名、第三引数はパラメータとして渡すもののジェネレータです。

GTestにはパラメータ生成のためのジェネレータが複数用意されていて、上の例で用いたValuesは引数に渡されたインスタンスを順次パラメータとして渡すようなものです。

この他にvectorなどのコンテナを取って、その中身をパラメータとして使うValuesInや、ジェネレータから任意の組み合わせを生成すいるCombineなどがあります。

詳しくはGTestのドキュメントを参照していただければと思います。

Travis CIでのvalue-parameterized test


さて、これで一応value-parameterized testは出来るようになったわけですが、これをTravis CI (Ubuntu 12.04 LTS)上で動かそうとすると若干の問題が生じます。

それは先ほど紹介したジェネレータの1つであるCombineを使う時に生じる問題です。

Combineは公式のドキュメントにある通り、生成されたパラメータの組み合わせをstd::tr1::tupleを使って表現します。

C++11ではstd::tupleに移行されていますので、例えばg++のオプションに-std=c++11などと入れようものなら、ビルドエラーになってしまいます。

いろいろとWebで調べてみるとtr1のtupleを使うかどうかを表すフラグとしてGTEST_USE_TR1_TUPLEといったものや、GTEST_HAS_OWN_TR1_TUPLEといったマクロをいじればいいと書いてあったのですが、どうもそれでは上手くいかない。

これとは別にGTEST_LANG_CXX11というマクロもあり、これでC++11を使ったテストができるのかと思いきやそれも上手くいかない。

で、結局何が問題だったのかというと、Ubuntu 12.04 LTSのapt-getで得られるGTestがバージョン1.6.0だったのが問題でした。

ということで、.travis.ymlで1.7.0をgit cloneして使うように設定を変更します。

install:
  # ... その他の処理

  # install GTest
  - sudo git clone --depth=1 -b release-1.7.0 https://github.com/google/googletest.git /usr/src/gtest
  - pushd /usr/src/gtest
  - sudo cmake .
  - sudo cmake --build .
  - sudo mv libg* /usr/local/lib;
  - sudo mv include/* /usr/include
  - popd

こうしてあげることで、GitHubのレポジトリからrelease-1.7.0というタグのついたソースが呼び出されて、ビルドおよびパスの通ったディレクトリへの移動が実行されます。

これに加えてCMakeなどでGTEST_LANG_CXX11の定義を入れてあげれば、無事Travis CI上でテストのビルドが通るようになります。

まとめ


Travis CI上でvalue-parameterized test、特にCombineジェネレータを使ったものを実行するには、

  • version 1.7.0のGTestを使う
  • GTEST_LANG_CXX11というマクロを定義する

という2つの処理を入れてあげればOKです。

というわけで長くなりましたが、今回の記事を終わります。最後までお読み頂きありがとうございました。

今回の記事で使ったPointクラス及び最終的なテストコードは以下に示しておきますので、よかったら参考にしてください。

ソースコード


main.cpp (テストを定義したメインのソースファイル)

#define GTEST_LANG_CXX11 1
#include "gtest/gtest.h"

#include <tuple>

#include "point.h"

class PointTest : public ::testing::Test {
protected:
    Point p0;

protected:
    PointTest() {}
    virtual ~PointTest() {}

    void SetUp() {
        p0 = Point(1, 2);
    }

    void TearDown() {
    }
};

using PointParams = std::tuple<Point, Point>;

class PointTestWithParam : public PointTest,
                           public ::testing::WithParamInterface<PointParams> {
protected:
    PointTestWithParam() {}
    virtual ~PointTestWithParam() {}
};

TEST_F(PointTest, Copy) {
    Point p1(p0);
    Point p2(p1);
    EXPECT_EQ(p0.x(), p1.x());
    EXPECT_EQ(p0.y(), p1.y());
}

TEST_P(PointTestWithParam, Assign) {
    Point p1 = std::get<0>(GetParam());
    Point p2 = std::get<1>(GetParam());
    Point p3 = p1 + p2;
    EXPECT_EQ(p1.x() + p2.x(), p3.x());
    EXPECT_EQ(p1.y() + p2.y(), p3.y());
}


std::vector<Point> pointParams = { Point(3, 4), Point(5, 6), Point(-1, 2) };

INSTANTIATE_TEST_CASE_P(, PointTestWithParam,
    ::testing::Combine(::testing::ValuesIn(pointParams),
                       ::testing::ValuesIn(pointParams)));

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

point.h (テスト対象のPointクラスを定義したヘッダファイル)

#ifndef _POINT_H_
#define _POINT_H_

class Point {
    int _x, _y;

public:
    Point(int x = 0, int y = 0)
        : _x(x)
        , _y(y) {
    }

    Point(const Point& p)
        : _x(p._x)
        , _y(p._y) {
    }

    ~Point() {
    }

    Point& operator=(const Point& p) {
        this->_x = p._x;
        this->_y = p._y;
        return *this;
    }

    inline int x() const { return _x; }
    inline int y() const { return _y; }
};

Point operator+(const Point& p, const Point q) {
    return Point(p.x() + q.x(), p.y() + q.y());
}

#endif  // _POINT_H_