目次
- 目次
- はじめに
- Google Test
- Google Testのインストール方法と実行方法(Linux, Mac)
- Google Testによるテストコードの書き方
- 最も簡単なテストコード
- 複数のテストで共通のコードを使う方法
- 再帰的にテストコードを検索し、テストを実行するPythonスクリプト
- モックとスタブ
- 参考資料
- MyEnigma Supporters
はじめに
下記のツイートのように、
最先端のロボットのシステムは非常に複雑なので、
きちんとソースコードのユニットテストを作成し、
CI(Continuous Integration)で自動テストをすることが、
当たり前になっているようです。
DRCの上位優勝チームは、言うまでもなくCIやTDDを当たり前のように実践していたのである。
— Shuuji Kajita (@s_kajita) 2016年2月20日
ihmcのAtlasの制御ソフトウエアはしっかりしたCIのもと開発された。僕らが出来ていなかったことの一つだ。/人型ロボット「Atlas」は、未だ「ロボット執事」には遠く(動画あり) https://t.co/UZWJQez4zQ
— Shuuji Kajita (@s_kajita) 2016年1月20日
自分も、複雑なシステムですが、
きちんと機能するロボットシステムを構築したいので、
ユニットテストをきちんとかけるように、
勉強をすることにしました。
今のところ、自分はC++とPythonを使用しているので、
今回は、ROSでも採用されている
C++のテストフレームワークである
Google Testを使ったC++コードのテスト方法について
説明しようと思います。
Pythonにおけるユニットテストの作成方法は、
下記の記事を参考にしてください。
Google Test
Google Testは、
Googleが開発しているC++用のユニットテストフレームワークです。
様々なプラットフォームで動作し、
高速なテストを実施することができます。
Google Testのインストール方法と実行方法(Linux, Mac)
現在、Google Testのソースコードは、
下記のgithubリポジトリで開発されているので、
まず初めにソースコードをチェックアウトします。
$ 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スクリプト
下記のPythonスクリプトは、
スクリプトを起動したディレクトリから再帰的に検索を実施し、
*Test.cpp (*は任意の文字)という名前のテストファイル検知し、
自動でテストを実施するスクリプトです。
前述のGTEST_DIRという環境変数が設定されていることを仮定しています。
下記のスクリプトを起動すると、
テストが失敗すると、
上記のように赤文字でエラーメッセージが表示されます。
このpythonスクリプトは下記のGitHubリポジトリでも管理されています。
#!/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 Supporters
もしこの記事が参考になり、
ブログをサポートしたいと思われた方は、
こちらからよろしくお願いします。