Google Testの単体テストカバレッジをCocoで取得する
4月 10, 2023 by Qt Group 日本オフィス | Comments
この記事はAchieving full code coverage of Unit Tests written with Google Testの抄訳です。
本稿では、Google Testフレームワークで記述されたユニットテストを、The Qt Companyが提供するコードカバレッジ分析ツールCocoと統合する方法をご紹介します。
ここでは大きく分けて、テストスイート全体のコードカバレッジを取得する方法と、各テストケースのカバレッジを個別に測定する方法をご紹介します。テストケースの内容に変更は必要なく、必要な作業はテストファイルの末尾に数行のコードを追加するだけです。
Google Testとは何か?
Google Testは、xUnitファミリーのテストフレームワークに属する、C++で記述されたソフトウェア向けのユニットテストフレームワークです。このようなフレームワークでは、テストをテストケースとして記述し、同様のテストケースをまとめてテストスイートを作成するのが通例です。
Google Testの特徴まとめ:
- xUnitテストフレームワークである
- テストディスカバリ機能
- 豊富なアサーションセット
- ユーザー定義アサーション
- Deathテスト機能
- 致命的な失敗と致命的ではない失敗
- 値パラメータ化テスト
- タイプパラメータ化テスト
- テスト実行のための様々なオプション
- XMLテストレポート作成
Google Testは、クラスや関数に対するユニットテストを作成する際に使用されます。テストしたいオブジェクトに対してテストスイートを記述し、それらとテストスイートをコンパイルしてリンクして実行可能なテストプログラムを作成することで、テストを実行することができます。
今回使用する例
Google Testの仕組みとコードカバレッジツールとの統合方法を説明するにあたり、今回は簡単な例を使用したいと思います。
テスト大量となる関数は、引数として受け取ったファイルパスの簡略化を行うものです。
- 親ディレクトリ参照(".")を含むUnixスタイルのファイルパスを受け取り、それらを取り除いたパスのバージョンを返す関数canonicalpath()がテストの対象です。
- この関数では、「abc/.../de」というパスは「de」に簡略化され、「.../abc」は変更されずに返されます。
canonicalpath関数の内容
関数canonicalpath() は、ファイル canonicalpath.cppの中で以下のように定義されています:
#include "canonicalpath.h"
#include <vector>
#include <regex>
#include <iostream>
std::string canonicalpath(const std::string &path)
{
if (path.length() <= 1)
return path;
std::string sep = path[0] == '/' ? "/" : "";
std::vector<std::string> entries;
std::smatch match;
std::regex re("[^/]+");
for (auto p = path; std::regex_search(p, match, re); p = match.suffix()) {
if (match.str() == ".." && !entries.empty()
&& !(sep == "" && entries.back() == ".."))
entries.pop_back();
else
entries.push_back(match.str());
}
std::string cpath;
for (auto s: entries) {
cpath += sep + s;
sep = "/";
}
return cpath;
}
canonicalpath.hというファイルもありますが、そちらの内容はとてもシンプルなので、ここでは説明を割愛します。
単体テストコードテストの内容は以下のファイル、canonicalpath_test.cpp に定義されてます:
#include "canonicalpath.h"
#include <gtest/gtest.h>
TEST(canonicalTests, relativePath) {
EXPECT_STREQ(canonicalpath("abc/de/").data(), "abc/de");
EXPECT_STREQ(canonicalpath("abc/../de").data(), "de");
EXPECT_STREQ(canonicalpath("../../abc").data(), "../../abc");
EXPECT_STREQ(canonicalpath("abc/../../../de").data(), "../../de");
EXPECT_STREQ(canonicalpath("abc/../de/../fgh").data(), "fgh");
}
TEST(canonicalTests, absolutePath) {
EXPECT_STREQ(canonicalpath("/abc/de/").data(), "/abc/de");
EXPECT_STREQ(canonicalpath("/abc/../de").data(), "/de");
EXPECT_STREQ(canonicalpath("/../../abc").data(), "/abc");
EXPECT_STREQ(canonicalpath("/abc/../../../de").data(), "/de");
EXPECT_STREQ(canonicalpath("/abc/../de/../fgh").data(), "/fgh");
}
TEST(canonicalTests, boundaryCase) {
EXPECT_STREQ(canonicalpath("").data(), "");
EXPECT_STREQ(canonicalpath("/").data(), "/");
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
このファイルには、3つのテストケースとmain()関数からなるテストスイートが含まれています。
TESTマクロは、最初のパラメータとしてテストスイート名を持ち、2番目のパラメータとしてテストケース名を持ちます。マクロの後の中括弧内のコードはテストコードです。テストケースの内容はすべて、canonicalpath()の戻り値と期待される結果をEXPECT_STREQマクロで比較しています。EXPECT_STREQ は char* 引数を必要としますが、canonicalpath() は std::string を返すので、その戻り値はメンバ関数 data() を使って char* に変換されています。
この3つのテストケースには、相対パス、絶対パスのテストに加え、非常に短いパスをテストデータとするテストが境界テストとして含まれています。
また、上記のファイルにはmain()関数を明示的に定義しています。Google Testには適切なmain()関数を提供するライブラリがあるため、すべてのテストプログラムにmain()関数が必要というわけではありませんが、Cocoの全機能を使用するために後でmain()関数内にコードを記述する必要があるため、ここでは明示的にmain()関数を定義しています。
テストプログラムのコンパイルと実行
Google Testは、特定のプラットフォームやコンパイラに依存しません。
そのため、すべてのシステムに対するコンパイル方法をここですべて紹介することはせず、その点はGoogle Test のドキュメントに説明を譲ることとします。
しかし、具体的な例を見ておくことはやはり有益であるため、ここではUnixでの例について見てみましょう。こちらの例では、テストをビルドして実行するための最小限のMakefileは次のようなものになります:
all: canonicalpath_tests
tests: all
./canonicalpath_tests
canonicalpath_tests: canonicalpath.o canonicalpath_tests.o
g++ $^ -o $@ -lgtest -lpthread
clean:
rm -f *.o
上記が行っている内容は、2つのソースファイルをコンパイルして正しいライブラリとリンクさせるという単純なものです。
[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up. [----------] 3 tests from canonicalTests [ RUN ] canonicalTests.relativePath [ OK ] canonicalTests.relativePath (1 ms)
[ RUN ] canonicalTests.absolutePath
[ OK ] canonicalTests.absolutePath (1 ms)
[ RUN ] canonicalTests.boundaryCase
[ OK ] canonicalTests.boundaryCase (0 ms)
[----------] 3 tests from canonicalTests (3 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 1 test case ran. (3 ms total)
[ PASSED ] 3 tests.
他のビルドシステムやプラットフォームでは厳密なビルド方法は異なるものの、出力は同じとなるはずです。
Cocoと連携させる
ここで、C、C++、SystemC、C#、Tcl、QMLのコードに対応したクロスプラットフォーム、クロスコンパイラのコードカバレッジ分析ツールであるCocoを使って、テストとプログラムのコードカバレッジを測定してみましょう。
プログラム全体をカバーする
Google Testはプラットフォームやビルドシステムに依存しないので、それぞれのビルドシステムに対応するコードのインストルメンテーションの方法については、ここではあまり詳細には触れません。
Cocoのドキュメントには、CMake(英語)、QMake(英語)、Visual Studio(英語)、Gnu Make(英語)、Microsoft NMake(英語)、その他いくつかのビルドシステムでビルドされたプログラムに対してカバレッジを計測する手順が含まれているので、お使いのビルドシステムに対応する章をご覧いただくことで、Cocoを有効にしてテストをコンパイルしていただくことができます。
Unix Makefileの場合、最も簡単な方法はすでにチュートリアルの1つ(英語)で紹介されています。ここでは、コードカバレッジを有効にしたコンパイルを実行するために必要となるすべての環境変数を、シェルスクリプトで設定しています。このスクリプトはinstrumentedというファイル名で保存されます。以下はinstrumentedの内容です。
#! /bin/bash
# Cocoのインストールフォルダにあるコンパイララッパーへのパスを追加
export PATH=/opt/SquishCoco/wrapper/bin:$PATH
# --cs-onは、アプリケーションのビルド時にCocoのカバレッジ計測を有効とするためのオプション。
# Cocoでは、COVERAGE_SCANNER_ARGSにオプションを指定することができる。
export COVERAGESCANNER_ARGS='--cs-on'
# --cs-exclude-file-abs-wildcardは、Cocoのカバレッジ計測に含めたくないファイルのパスを指定する。
export COVERAGESCANNER_ARGS+=' --cs-exclude-file-abs-wildcard=*/canonicalpath_tests.cpp'
"$@"
これを使えば、「./instrumented make clean tests」を実行することでテストを再ビルドし、Cocoのカバレッジ計測を有効にした状態で再び実行することができます。
この手順を踏むことで、canonicalpath_tests.csexeとcanonicalpath_tests.csmesという2つの新しいファイルが生成されます。最初のファイルにはカバレッジの測定値が、2番目のファイルにはその測定値を解釈するためのデータが含まれています。この2つのファイルをそれぞれ.csexeファイルと.csmesファイルと呼ぶことにします。
他のビルドシステムでも、詳細は大きく異なるものの、常にCoverageScannerフラグ(英語)の設定と完全な再コンパイルを行う処理を含める必要があります。
フラグ --cs-on はコードカバレッジの測定を有効にし、 --cs-exclude-file-abs-wildcard=*/canonicalpath_tests.cpp はテストケースの定義のあるファイルをコード・カバレッジ測定の対象から除外しています。今回は、 canonicalpath.cpp のコード・カバレッジの結果だけに関心があるためです。
※Windowsシステムでは、カバレッジファイルの名前は、canonicalpath_tests.exe.csexeとcanonicalpath_tests.exe.csmesとなります。
各テストのカバレッジを個別に測定する
現在の設定では、テストスイートのすべてのテストについて、canonicalpath() のコードカバレッジを一気に取得しています。
しかし、各テストのカバーしたカバレッジを個別に取得することで、コードのそれぞれの行がどのテストによって実行されたかを確認することも可能になります。
そのためには、CocoとGoogle Testの2つの機能を組み合わせる必要があります:
- Cocoは、インストルメント対象のアプリケーションコードにライブラリ関数を提供しています。これらを使って、アプリケーションはテスト名、その結果、また蓄積されたカバレッジ測定値を.csexeファイルに保存することができます。
- 一方、Google TestにはTestEventListenerクラスがあり、各テストケースの前後に特定の処理を実行したり、テスト名やその結果を取得したりすることが可能です。
Event Listenerクラス
これらの機能を使うには、canonicalpath_tests.cppにCocoライブラリの関数を呼び出すTestEventListenerのサブクラスを作成する必要があります。クラス定義は、ファイルの最後、main()関数の直前に置いています。以下が、クラス定義のコードです:
//インストルメントが有効な場合のみ、TestEventListenerのサブクラスを定義する
#ifdef __COVERAGESCANNER__
class CodeCoverageListener : public ::testing::TestEventListener
{
public:
virtual void OnTestProgramStart(const ::testing::UnitTest&) {} virtual void OnTestIterationStart(const ::testing::UnitTest&, int) {}
virtual void OnEnvironmentsSetUpStart(const ::testing::UnitTest&) {}
virtual void OnEnvironmentsSetUpEnd(const ::testing::UnitTest&) {}
virtual void OnTestCaseStart(const ::testing::TestCase&) {}
virtual void OnTestPartResult(const ::testing::TestPartResult&) {}
virtual void OnTestCaseEnd(const ::testing::TestCase&) {}
virtual void OnEnvironmentsTearDownStart(const ::testing::UnitTest&) {}
virtual void OnEnvironmentsTearDownEnd(const ::testing::UnitTest&) {}
virtual void OnTestIterationEnd(const ::testing::UnitTest&, int) {}
virtual void OnTestProgramEnd(const ::testing::UnitTest&) {}
//テストケース開始時に実行される関数
virtual void OnTestStart(const ::testing::TestInfo& test_info)
{
__coveragescanner_clear();
std::string test_name = std::string(test_info.test_case_name())
+ '/' + std::string(test_info.name());
//.csexeファイルにテスト名を記録する
__coveragescanner_testname(test_name.c_str());
}
//テストケース終了時に実行される関数
virtual void OnTestEnd(const ::testing::TestInfo& test_info)
{
__coveragescanner_teststate("UNKNOWN");
//.csexeファイルにテスト結果を記録する
if (test_info.result()) {
if (test_info.result()->Passed())
__coveragescanner_teststate("PASSED");
if (test_info.result()->Failed())
__coveragescanner_teststate("FAILED");
}
//.csexeファイルにここまでの内容を保存する
__coveragescanner_save();
}
};
#endif
CodeCoverageListenerクラスは、#ifdefと#endifブロックで囲まれており、コードカバレッジ計測が有効になっているときだけコンパイルされるようになっています。(シンボル __COVERAGESCANNER__ はCocoによって自動的に定義されます)
Cocoに限った話をすれば、メンバ関数OnTestStartとOnTestEndの実装があれば問題ありません。しかし、Google Testでは、TestEventListenerの多くのメンバー関数が純粋な仮想関数として宣言されているので、それらのすべての実装を提供しなければなりません。そのため、未使用の関数の実装は、クラス定義の冒頭のブロックに記述されています。
OnTestStart()関数ではまず、ここまで記録されたすべてのカバレッジカウンターをゼロにリセットし、ひとつ前のテストケースのカバレッジの内容が今回のテストケースの内容として記録されないようにしてします。
その後、テストケース名を .csexe ファイルに書き込みます。
上記の実装では、名前は「testsuite/testcase」という形式になっていますが、これを変更して独自の形式として定義することも可能です。Cocoは、記録されるテスト名に含まれているスラッシュを使用して、のちに登場するCoverage Browserでテストケースを階層的に表示します。
OnTestStop()は、テスト結果を.csexeファイルに書き込みます。結果は"PASSED"、"FAILED"、あるいは "UNKNOWN "としています。
main() 関数
新しく作成したCodeCoverageListenerクラスは、Google Testに登録する必要があります。これはmain()関数の中で次のように行われています:
int main(int argc, char **argv)カバレッジ結果の確認
{
#ifdef __COVERAGESCANNER__
::testing::UnitTest::GetInstance()->listeners()
.Append(new CodeCoverageListener);
#endif
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
コードを再コンパイルして再度実行すると、.csmesと.csexeファイルが生成されるので、それをCoverageBrowserに読み込むことで結果を確認することができます。読み込みを行うと、以下のような結果が表示されるはずです。

左側の「Executions」ウィンドウには、テストスイートの名前とテストケースが表示されています。ここでは、テストケースがテストスイートの1つ下の階層に表示される階層ビューモードを選択しました。その右側にはテスト結果が表示されており、すべてのテストケースが「PASSED」となっています。
テストケースごとにカバレッジの測定値が分かれているので、「Executions」ウィンドウで選択した1つまたは複数のテストケースのカバレッジのみを確認することができるようになりました。上の画像では、テストケース「relativePath」のカバレッジのみを選択しています。
今回の例と同様のステップを踏むことで、他のGoogle TestプロジェクトもCocoでインストルメンテーションを行い、そのコードカバレッジを測定することができます。
お問い合わせ
CocoをはじめとするQtのQA(品質保証)ツールにご興味がおありの方は、Qt JapanのEメールアドレス:japan@qt.ioまでお気軽にご連絡ください。概要のご説明から詳細な技術的相談、また無料のツールトライアルのご案内もいたしております。
Blog Topics:
Comments
Subscribe to our newsletter
Subscribe Newsletter
Try Qt 6.9 Now!
Download the latest release here: www.qt.io/download.
Qt 6.9 is now available, with new features and improvements for application developers and device creators.
We're Hiring
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.
This is a very welcome addition to Shapes and will make me consider using it in more areas. Up until now I was almost forced to resort to QNanoPainter for properly antialiased lines without resorting to very expensive SSAA approaches. When trying the example and looking at the frametimes (when forced constant rendering with frameanimation), I am seeing a lot of constant stutters though with the new renderer. Even with a stationary screen these stutters are there. Use a frametime graph tool like Rivatuner statistics server with afterburner to see it better. Are these stutters something you are looking into?
Thanks for the feedback!
I do see stutter when clicking on a country and zooming on it. This is due to creating the shapes for the country name and local weather symbols while the zoom is animating. Triangulation currently happens on the main thread and will block it for a frame or two. We are working on implementing support for the asynchronous flag, as well as better parallelization of the preprocessing, which should also reduce the cost of this step. In general, this code also benefits well from optimized builds, so for performance testing make sure you use a release build.
If the stutter happens just by forcing constant updates on the main scene, then we would be interested in hearing more about that. If you can make a bug report with some additional information on this, it would be very helpful for our investigation. If you can, please include a description of the modifications you made to the example, information on your platform (OS and GPU) as well as information about 1. whether this only happens with the new Curve renderer or also with the default Geometry renderer and 2. Whether it helps to change the RHI backend to something else.
Thank you!
Thanks for the info. I have made this bugreport: https://bugreports.qt.io/browse/QTBUG-117901 . It contains a video as attachment where I try to show it. Appreciate your nice work, would be great to see if you can identify where the stutters come from
It seems that "Right to left" under pics should be "Left to right".
Thank you! Fixed now
Is there anyway to print Shapes? Or export them to PDF or SVG? This is supported with QPainter.
No, sorry, printing still has to go through QPainter. We do want to make it easier to transport QPainterPaths between C++ and QML which would make this type of use case easier.