先日の記事、
「現場に必要なOSS (オープンソースソフトウェア) のテストツールとは」
で触れた、無償のテストツールをいくつか紹介していくシリーズ。
今回は、本格的なテストフェーズで使える「単体テストツール」として、
『 Google Test 』
を使ってみたいと思います。
設計フェーズで使える無償ツール『PlantUML』を使ってみた記事はコチラ ↓ から。
CONTENTS
単体テストツールとは?
ツールの話の前に「単体テスト」について、少し説明したい思います。
単体テストとは
ソースコードの個々のユニット、すなわち、1つ以上のコンピュータプログラムモジュールが使用に適しているかどうかを決定するために、関連する制御データ、使用手順、操作手順とともにテストする手法である。
@Wikipedia
と定義されています。
単体テストツールとは簡単に言うと、
単体テスト(ユニットテスト)を簡単に行うためのツール
です。
極端な話ですが、
単体テスト自体は、紙とペンによる机上確認や、デバッガーを用いて1個1個値を設定しての確認することでも実施できます。
ただ、膨大な関数をこの方法でテストするというのは時間や手間が掛かりすぎるため、現実的ではありません。
単体テストツールを使うと、
各関数の単体テストとして用意したテストケースに沿って、テストを実行し、結果をアウトプットしてくれます。
つまり、単体テストツールを利用する事で以下の2点が実現できます。
- ・テストを実行するだけで結果がアウトプットされる → 『テストの自動化』
- ・一度作ってさえしまえば何度でもテストできる → 『テストの再現性の確保』
単体テストの用語
単体テストツールを使うにあたって、よく使われる用語について確認してみましょう。
ドライバ
ドライバとは、テスト対象の関数を呼び出す上位関数の代用となる仕組みを指します。
スタブ
テスト対象関数内で呼ばれる関数や変数の代わりになる仕組みを指します。
テスト対象の関数に参照するグローバル変数や値を参照する関数などがあれば、スタブを作って対応する必要があります。
基本的にテストを進めるために決まった固定値を出力します。
モック
テスト対象関数内で、呼ばれる関数や変数の呼び出しや呼び出し方に応じた値やエラーを返す仕組みを指します。
モック機能は、単体テストツールによって機能を持たない場合や、他のツールとの組み合わせで実現している場合があります。
実装する場合、ある程度元々よばれる処理を簡易化した処理を行うため、元々の処理自体の内部的な知識が必要です。
スタブと違い、意識する対象は呼び出し側の入力が正しいかを意識した形になります。
例えば「ある処理が複数回呼ばれる場合に、呼ばれた回数や引数を記録し引数に応じて出力が変わるようなテストをしたい」といった場合は、モック機能の利用が必要になるでしょう。
OSSにはどんなツールがあるか
単体テストツールは、有償のものと無償のものに大別できます。
有償のものは、
先ほどのドライバ・スタブ/モックなどが予め用意されていたり、
GUI上に入力するだけで単体テストコードが生成されるなど、
高度な機能を有しております。
一方で、OSSやフリーのものは、
ドライバ・スタブ/モックの実装やテストコードの作成は自身で行う必要があります。
今まで単に「単体テストツール」と呼んでおりましたが、実際には
前者が「単体テストツール」と呼ばれ、
後者は「単体テストフレームワーク」と呼ばれることが多いです。
単体テストツール/フレームワーク共に、多種多様なものがあり、
Wikipediaでも一部が一覧でまとめられています。
これらの中でも、
- ・実績があるもの
- ・開発が継続しているもの
- ・組込みでも使えるもの
となると数が限られてきますが、
現在代表的に上がるものとしては、以下の3つではないでしょうか。
Google Test(Google C++ Testing Framework)
GoogleがGitHubで提供している単体テストフレームワーク。
日本語ドキュメントやサンプルコードなどもあり情報も探しやすい印象です。
Google Mockというモック機能も対応していますが、対応しているのがC++のオブジェクトになるためC言語の関数呼び出しは通常対応できない問題があります。
ただ、C++11以上の環境である必要があることなど、C言語よりはC++を主としている印象があり、開発環境やテストコードなどによってはそのままでは対応できない場合もあります。
CppUTest
GitHub上で公開されているOSS単体テストフレームワーク。
日本語ドキュメントなどはありませんがGoogle Testほどではないにせよ情報は探しやすい印象です。
CppUMockというモック機能がデフォルトで組み込まれており、C言語関数向けのモックインターフェースもあるため組込み向けのソースコードに適応しやすそうな印象を受けます。
C++環境ベースになるので、ビルド環境がC++でも問題ないという方であれば一番使いやすいかと思います。
Unity(+CMock)
こちらもGitHub上で公開されている、単体テストフレームワーク。
同じ名前の有名なクロスプラットフォーム統合開発環境があるため情報を探しにくい印象があります。
別途導入する必要がありますがCMockというモック機能と連携でき、すべてC言語で書かれているため組込み向けのソースコードにも適用しやすそうな印象を受けます。
C言語ベースの為、ビルド環境含めてCの知識の範囲のみで対応できるのが特徴になります。
さっそく導入してみた
今回はVMWare上に構築したUbuntu内で、Google Testの入手からサンプルコードの実行までをやってみたいと思います。
ソースコードの入手
上記のGitからZipファイルを入手するか、Gitからソースコードを入手します。
以下のLinuxコンソールでの作業です。
>sudo apt install git … Gitからcloneする場合は必要 >sudo apt install cmake … CMakeを利用してビルドする場合は必要 >git clone https://github.com/google/googletest.git … Gitからソースコードを入手
Gitからインストールする場合、あらかじめGitをインストールしておきます。
またGoogle TestはCMakeから楽にビルドできるようになっているため、CMakeも併せてインストールします。
ライブラリのビルド
環境によってC++ビルド環境(g++等)がインストールされていない場合があります。
ビルド環境がない場合CMake実行時にビルドエラーとなるため、あらかじめ作業中のLinux環境でのビルド環境を入手しておく必要があります。
もしビルドエラーが発生した場合はbuildフォルダ以下を削除し、エラーを解決した後にbuildフォルダの作成からやり直してください。
(若干ハマりました…。)
>sudo apt-get update && sudo apt-get install build-essential … 環境によってはg++(gcc)のインストールが必要 >mkdir build && cd build … ビルド後のオブジェクトファイル用のディレクトリを作成し、移動 >cmake .. … ルートフォルダ(この場合はgoogletestフォルダ)のCMakeLists.txtのある階層を指定してCMakeビルド >make … buildフォルダ上にビルド環境とMakefileが生成されるのでMakeする Scanning dependencies of target gtest … [100%] Built target gtest_main … これが表示されればビルド完了
ビルドに成功すると、./build/lib フォルダ以下にライブラリファイルが生成されます。
他の環境から参照できるように、必要に応じてGoogle Test用のヘッダファイルとライブラリを、共有ヘッダ・ライブラリリンクに追加します。
>sudo cp -r ~/googletest/googlemock/include/gmock /usr/local/include/gmock >sudo cp -r ~/googletest/googletest/include/gtest /usr/local/include/gtest >sudo cp ~/googletest/build/lib/*.a /usr/local/lib/
サンプルコードの実行
テストコードの参考としてサンプルコードが用意されていますので、
それをビルドして実行してみます。
サンプルコードと、対応するテストコードは以下のようになっています。
階乗をする関数Factorialに対して3つのテストパターンが用意されています。
#include "sample1.h" // Returns n! (the factorial of n). For negative n, n! is defined to be 1. int Factorial(int n) { int result = 1; for (int i = 1; i <= n; i++) { result *= i; } return result; }
//対象関数のプロトタイプ宣言が必要 #include "sample1.h" //googletestのヘッダ #include "gtest/gtest.h" //TESTマクロの第一引数がテストケース名、第二引数がテスト内容 //EXPECT_EQ()がアサーション(テスト判定処理) TEST(FactorialTest, Negative) { EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); } // Tests factorial of 0. TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); } // Tests factorial of positive numbers. TEST(FactorialTest, Positive) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); }
テスト判定には”アサーション”と呼ばれる判定用のマクロを利用します。
マクロ名などの細部は異なりますが、これは他の単体テストフレームワークも同様です。
例えば上記のEXPECT_EQ()は第一引数が期待値、第二引数が判定したい処理を表します。
EXPECT_EQ(2, Factorial(2))であればFactorial関数の引数に2を代入して実行したとき、
その戻り値が2であるかどうかを判定する処理となります。
EXPECT_GT(Factorial(-10), 0)であれば、
第一引数の結果>第二引数であるかを判定する処理、という形です。
その他、文字列の比較やNULLチェックなど、様々なアサーションが用意されています。
GoogleTestの場合、TESTマクロで定義した各ユニットテストは自動的にテスト実行時の対象として組み込まれます。
(テストディスカバリ機能といいます)
では実際にビルドして実行してみましょう。
>cd ~/googletest/googletest/samples/ … ソースコードフォルダに移動 >g++ sample1.cc sample1_unittest.cc -pthread -lgtest_main -lgtest … g++でビルド
sample.ccがテスト対象のコード、sample1_unitest.ccが単体テストコードになります。
また、Google Testでのビルドに必要なオプションのとして 以下の指定が必要になります。
-lpthread … マルチスレッドオプション -lgtest …Google Testライブラリの参照 -lgtest_main … 単体テストのmain処理参照(main関数を自作しない場合必要)
ビルドに成功すると単体テスト用のオブジェクトが生成されますので早速実行してみます。
使ってみた感想
ネット検索でも一番情報量の多いと思われるGoogle Testを導入してみました。
動かしてみるところまでは簡単でしたが、組込み開発の場合、その後が本当に大変になります。
本来は各開発環境で使用しているレジスタやAPIを、
スタブ、あるいはモックとして定義していく作業が必要になります。
またこれは単体テストに限った話ではないですが、
レガシーコードをどうするかといった問題もあります。
単体テストではテストコードを書く必要がありますから、
レガシーコードに対してもテストコードを用意していくのはかなり時間がかかるだろうという印象を受けました。
有償ツールの場合、特に組込み系対応のツールであれば、マイコンやコンパイラ単位の対応として、
レジスタやAPIなどのスタブ/モックの生成が、GUI上で期待値を書くだけでコード生成されたりと、
運用開始のサポートされている場合が多く、環境に適応させるための初期コストはある程度軽減できると思います。
一方、最初からAPI制御が前提でスタブ/モック化がしやすいLinux環境での開発やアプリケーション層のみの開発であれば、
元々想定ソースコードに近い事もありまだ適用しやすいと思われます。
まとめ
単体テストは、手元に開発機器の本体がなくてもソースコードをテストできる手段の1つであるため、
現在の状況の中での重要度は高まっていると言えるかもしれません。
OSSな単体テストツールであっても、
導入手順やテストコードの作成のハードルは大きく下げられており、準備まではかなり容易になっているといえます。
とはいえ、いざ運用となるとテストコードを書く必要がある分負担も大きく、
レガシーコードも含めて一気に適用しすぐに進められるものではないと思います。
- 新規、または変更した部分などから始める
- パイロットプロジェクトで運用方法を固め、少しづつ運用するプロジェクトを増やしていく
といった、適応範囲を慎重に広げていく必要があるでしょう。