MyEnigma

とある自律移動システムエンジニアのブログです。#Robotics #Programing #C++ #Python #MATLAB #Vim #Mathematics #Book #Movie #Traveling #Mac #iPhone

ロボティクスのためのGoogle TestによるC++コードユニットテスト

目次

はじめに

下記のツイートのように、

最先端のロボットのシステムは非常に複雑なので、

きちんとソースコードのユニットテストを作成し、

CI(Continuous Integration)で自動テストをすることが、

当たり前になっているようです。

 

自分も、複雑なシステムですが、

きちんと機能するロボットシステムを構築したいので、

ユニットテストをきちんとかけるように、

勉強をすることにしました。

 

今のところ、自分はC++とPythonを使用しているので、

今回は、ROSでも採用されている

C++のテストフレームワークである

Google Testを使ったC++コードのテスト方法について

説明しようと思います。

github.com

 

Pythonにおけるユニットテストの作成方法は、

下記の記事を参考にしてください。

myenigma.hatenablog.com

 

Google Test

Google Testは、

Googleが開発しているC++用のユニットテストフレームワークです。

github.com

 

様々なプラットフォームで動作し、

高速なテストを実施することができます。

 

Google Testのインストール方法と実行方法(Linux, Mac)

現在、Google Testのソースコードは、

下記のgithubリポジトリで開発されているので、

まず初めにソースコードをチェックアウトします。

github.com

$ git clone https://github.com/google/googletest gtest

 

続いて、下記のコマンドでコードをコンパイルします。

$ cd gtest

$ cmake .

$ make

 

続いて、

先ほどのコンパイルしたコードの場所を環境変数として登録します。

(環境変数に登録しなくても使えますが、

後述する自動テスト検索スクリプトを使うために必要です)

.bashrcや.bash_profileに下記を追記しましょう

$ export GTEST_DIR='/Users/Hoge/gtest'

 

最後に後述の方法で、テストコード(ここではtest.cpp)を書き、

下記のコマンドを実行すれば、テストコードがコンパイルされ、実行されます。

$ g++ test.cpp -I./gtest/googletest/include -L./gtest/googlemock/gtest -lpthread -lgtest_main -lgtest && ./a.out

 

Google Testによるテストコードの書き方

下記で簡単なGoogle Testのテストコードの書き方を紹介します。

 

二種類のアサーション

Google TestにはASSERT_*というアサーションと、

EXPECT_*というアサーションがあり、

ASSERT_*はテストが失敗すると、その後のテストは実施されず、

EXPECT_*はテストが失敗しても、その後のテストは実施されます。

 

アサーションコード

下記の表はGoogle Testのアサーションコードです。

致命的なアサーション 致命的ではないアサーション テスト内容
ASSERT_TRUE(condition) EXPECT_TRUE(condition) condition が true
ASSERT_FALSE(condition) EXPECT_FALSE(condition) condition が false
ASSERT_EQ(expected, actual) EXPECT_EQ(expected, actual) expected == actual
ASSERT_NE(val1, val2) EXPECT_NE(val1, val2) val1 != val2
ASSERT_LT(val1, val2) EXPECT_LT(val1, val2) val1 < val2
ASSERT_LE(val1, val2) EXPECT_LE(val1, val2) val1 <= val2
ASSERT_GT(val1, val2) EXPECT_GT(val1, val2) val1 > val2
ASSERT_GE(val1, val2) EXPECT_GE(val1, val2) val1 >= val2
ASSERT_STREQ(expected_str, actual_str) EXPECT_STREQ(expected_str, actual_str) 2つの C 文字列の内容が等しい
ASSERT_STRNE(str1, str2) EXPECT_STRNE(str1, str2) 2つの C 文字列の内容が等しくない
ASSERT_STRCASEEQ(expected_str, actual_str) EXPECT_STRCASEEQ(expected_str, actual_str) (大文字小文字を無視)2つの C 文字の内容が等しい
ASSERT_STRCASENE(str1, str2) EXPECT_STRCASENE(str1, str2) (大文字小文字を無視)C 文字列の内容が等しくない

 

最も簡単なテストコード

下記のコードは最も基本的なテストコードになります。

funcという関数に1を与えた時に、1が帰ってくることをテストしています。

 

#include "gtest/gtest.h"

TEST(test_case_name, test_name) {
   EXPECT_EQ(1, func(1))
}

google testではTESTという関数を書いて、

引数の一番目がテストグループ、

二番目がテスト名になるようにテストを記述します。

一つのテストに複数のアサーションを入れてもOKです。

 

複数のテストで共通のコードを使う方法

複数のテストにおいて、

共通のオブジェクト生成などを実施する場合は、

同じコードを使いまわすと

テストコードも重複がなくなります。

同様に、それぞれののテストが終わった後に、

リソースの開放などを実施したい場合もあるでしょう。

 

そんな時は、下記の記事の通り、

::testing::Testを継承したクラスを宣言し、

TESTマクロの第一引数に、そのクラスを指定することで、

クラスのメンバ変数に、複数のテストコードから

アクセスすることができます。

 

このSetUp関数とTearDown関数は

それぞれのテストの際に、毎回実施されるので、

共通のデータ・セットアップなどに利用可能です。

class TestFix : public ::testing::Test
{
protected:
    std::vector<int> data;
 
    virtual void SetUp(){
    //テスト用データの作成
        data.push_back(0);
        data.push_back(1);
        data.push_back(2);
    }
    virtual void TearDown(){
        //リソースの開放
    }
}:
TEST_F(TestFix, Test1)
{
    data.push_back(3);  // TestFix のメンバーにアクセス可能
    TEST_ASSERT_EQ(3, data.size());
}
TEST_F(TestFix, Test2)
{
    TEST_ASSERT_EQ(3, data.size());
}

 

再帰的にテストコードを検索し、テストを実行するPythonスクリプト

f:id:meison_amsl:20160302203251p:plain

 

下記のPythonスクリプトは、

スクリプトを起動したディレクトリから再帰的に検索を実施し、

*Test.cpp (*は任意の文字)という名前のテストファイル検知し、

自動でテストを実施するスクリプトです。

前述のGTEST_DIRという環境変数が設定されていることを仮定しています。

 

下記のスクリプトを起動すると、

テストが失敗すると、

上記のように赤文字でエラーメッセージが表示されます。

 

このpythonスクリプトは下記のGitHubリポジトリでも管理されています。

github.com

 

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author:Atsushi Sakai
import os
import sys
import logging as log
import subprocess 

#You can set top dir of the test file search
SearchPath="."

def Print(string, color, highlight=False):
    """
    Colored print
    colorlist:
        red,green
    """
    end="\033[1;m"
    pstr=""
    if color == "red":
        if highlight:
            pstr+='\033[1;41m'
        else:
            pstr+='\033[1;31m'
    elif color == "green":
        if highlight:
            pstr+='\033[1;42m'
        else:
            pstr+='\033[1;32m'
    elif color == "yellow":
        if highlight:
            pstr+='\033[1;43m'
        else:
            pstr+='\033[1;33m'

    else:
        print(("Error Unsupported color:"+color))

    print((pstr+string+end))

class Gtest:
    def __init__(self):
           # Get gtest dir path
        self.gtestdir=os.environ.get("GTEST_DIR")
        if self.gtestdir==None:
            log.critical('Cannot find GTEST_DIR environment variable')
            exit(0)

        testPaths=self.SearchTestFiles()

        for path in testPaths:
            self.ExeGTest(path)

    def ExeGTest(self,path):
        """
            Execute gtest
            sample:g++ -lpthread test.cpp -I./gtest/include  -L./gtest/mybuild -lgtest_main -lgtest && ./a.out 
        """
        print(self.gtestdir)
        cmd="g++ -lpthread "
        cmd+=path
        cmd+=" -I"
        cmd+=self.gtestdir+"/googletest/include -L"
        cmd+=self.gtestdir+"/googlemock/gtest "
        cmd+=" -lgtest_main -lgtest && ./a.out"
        print(cmd)
        try:
            output = subprocess.check_output(cmd,shell=True)
            returncode = 0
            Print(output,"green")
        except subprocess.CalledProcessError as e:
            output = e.output
            returncode = e.returncode
            Print(output,"red")

    def SearchTestFiles(self):
        """ 
        Search test file
        """
        testPaths=[]
        for file in self.fild_all_files(SearchPath):
            if "Test.cpp" in file:
                testPaths.append(file)

        if len(testPaths)==0:
            Print('Cannot find any test file.',"yellow")
            exit(0)

        print((str(len(testPaths))+" test files are found"))
        print(testPaths)
        return testPaths

    def fild_all_files(self,directory):
        for root, dirs, files in os.walk(directory):
            yield root
            for file in files:
                yield os.path.join(root, file)


if __name__ == '__main__':
    print(__file__+" start!!")
    argvs = sys.argv 
    if len(argvs)>=2:
        SearchPath=argvs[1]
    Gtest()

モックとスタブ

ユニットテストでは、モックとスタブという言葉がよく使われますが、

モックは、目的のメソッドがテスト中に呼び出されたかを確認する手法で、

スタブは、予測が難しいメソッドを、仮の実装で予測可能な値を返すことで、

ユニットテストをしやすくする手法です。

 

参考資料

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

 

MyEnigma Supporters

もしこの記事が参考になり、

ブログをサポートしたいと思われた方は、

こちらからよろしくお願いします。

myenigma.hatenablog.com