GoogleTestとFakeFunctionFrameworkによるTOPPERSアプリケーションの単体テスト自動化
パイオニアシステムテクノロジー株式会社 2022年度新入社員一同作成者:仙石 友輝
共同作業者:薄井 茜、髙橋 佑梨、村山 晴基

目次• 著作権表記• 参考サイト、参考資料
1.概要
2.テストの全体像
3.テスト対象
4.テスト環境の構築
5.テストケース
5.1.各テストケースに共通する箇所の解説
5.2.テストケースの解説(1)スイッチスキャンタスク
5.3.テストケースの解説(2)LED4点滅タスク

6.実行ファイルの生成
7.テストの自動実行
8.テスト結果
9.テストの応用
10.まとめ著作権表記• 本資料は、名古屋大学情報学研究科附属組込みシステム研究センター(NCES)の「組込みソフトウェア開発技術の基礎」(以降、「組込みソフトウェア開発技術の基礎」と略) を改変・再配布する形になるため、著作権を以下のとおり示します。

参考サイト、参考資料
・組込みソフトウェア開発技術の基礎 https://www.nces.i.nagoya-u.ac.jp/NEP/materials/about.html・GoogleTest https://github.com/google/googletest
・GoogleTest ドキュメント日本語訳 http://opencv.jp/googletestdocs/index.html
・Fake Function Framework(FFF) https://github.com/meekrosoft/fff
・モダンC言語プログラミング https://tatsu-zine.com/books/modern-cprogramming
出版社: KADOKAWA/アスキー・メディアワークス

1.概要(1/5)• 目的•

組込みソフトウェア開発では、実機が用意されないと通信系やレジスタ・メモリ系のテストが困難です。ソースコードを改変すれば出来なくもないですが、製品に載せるソースコードに手を加えることは避けたいです（品質が担保できなくなる）。ソースファイルを疑似テスト用と本番用で入れ替えすることも考えられますが、かなり手間となります。

これらの課題を解決するために、実機不要な自動単体テスト環境をGoogleTest ＋ FFF（Fake Function Framework）によって実現する

1.概要(2/5)• 狙い• 昨今の半導体不足や短納期開発における実機不足の改善。
• 誤ってテストコードが本番用ソフトに混入することの防止。
• いきなり大規模開発に単体テスト自動化を適用しようとすると失敗しがちのため、先ず小規模なカップラーメンタイマに適用することで、基本を理解する。

1.概要(3/5)• GoogleTestとは• Google社の提供するC++用の単体テストのフレームワーク• C言語プログラムの単体テストにも利用可能• GoogleTestの利点• C++言語の知識がなくても、容易に単体テスト環境の構築が可能• 関数の戻り値や変数の検証が容易• テストケースの追加が容易• テスト結果の可読性が高い• 日本語のドキュメントが充実• 非常に大きい回数の繰り返しテストが高速かつ,容易に実装が可能• テスト対象を改変せずに試験が可能

1.概要(4/5)• Fake Function Framework (FFF) とは• FFFは、単体テスト用のダミー関数を自動生成するツールです。FFFは、GoogleTestと組み合わせて動作します。

• Fake Function Framework (FFF) の利点• C++の知識が浅くても下位関数を容易に偽装でき、戻り値も指定可能• レジスタの偽装ができるので、実機がなくてもPC上で単体テストが可能• カスタムFake関数にて、独自に作成したモック関数を実行可能• TOPPERSのサービスコールの偽装や、コールバック関数の偽装も可能• GoogleTestの豊富な判定文を用いて、偽装した下位関数の呼び出し回数や下位関数に渡す引数等の検証が可能• GoogleTestと組み合わせて、テスト対象を改変せず、高速に試験可能

1.概要(5/5)• GoogleTest + FFF をTOPPERSアプリケーションの単体テストに適用するために• setjmp/longjmp を用いた無限ループからの脱出• 無限ループを含む関数の呼び出しがあると、テストを完了できない。
• テスト実行前にsetjmpをコール。
• TOPPERSのサービスコールを偽装。偽装したサービスコール内にlongjmp を仕込むことで無限ループからの脱出を可能とし、正しくテストが行えるようにする。

• 再現性の低いバグを発見可能なテストケース• テストケースのテンプレートを作成し、数多の組み合わせを検証することで、特定の条件でのみ発現するバグを発見することが可能。
• SW1とPUSH1の同時変化といった実機ではなかなか再現できないテストが可能。

2.テストの全体像
テストケース
作成• GoogleTest:テストケース,期待値検証マクロの提供• FFF:関数の偽装マクロの提供

テスト実施• テスト対象:GoogleTestからコールされ、FFFで偽装された関数をコールし処理を終える• GoogleTest:期待値を検証しながら,複数個あるテストケースを1つずつ逐次的に実行

テスト終了• GoogleTest:テスト結果レポート作成

3.テスト対象(1/7)• 今回のテスト対象はカップラーメンタイマのソースコードです。このカップラーメンタイマはTOPPERSのアプリケーションタスクとして実装されております。カップラーメンタイマのソースコードや、外観、仕様、開発環境等は、第11回TOPPERS活用アイデア・アプリケーション開発コンテストの「データキュー機能と固定長メモリプール機能を用いたタスク間通信に関する教材」を参照願います。https://www.toppers.jp/contest.html

3.テスト対象(2/7)• ソースファイル分割• GoogleTestはファイル単位でテストを実行します。
• 「第11回TOPPERSアイディア・アプリケーション開発コンテスト」にて、弊社の2021年度の新人が作成したカップラーメンタイマは、ファイル分割の粒度が粗すぎることから、テスト向きではないです。
• そこでファイルを分割し、テストをしやすい構成にしました。

3.テスト対象(3/7)
blink.c• 分割前のファイル構成
起動・停止要求

1秒周期ハンドラ

点灯・消灯要求

起床要求
1秒経過イベント
生成タスク

タイマタスク
イベン
トイベント

0.01秒
周期ハンドラ

起床要求

LED4点滅タスク

LED4点滅イベン
イベント ト送信タスク.

スイッチイベント
生成タスク
状態取得

起動・停止要求

点灯・消灯要求

LED２

プッシュスイッチ

スライドスイッチ

timer.ccup_timer.c

0.25秒周期ハンドラ

1秒周期
ハンドラ

LED1

起床要求

点灯・消灯要求13

3.テスト対象(4/7)• 分割後のファイル構成（計時関連）
ベース周期(1秒)生成関数群(base_time_cyclic.c)

タイマタスクメイン関数(timer_main.c)

1秒周期ハンドラスイッチスキャン関数群(sw_state_cyclic.c)
イベント

起床要求

0.01秒周期ハンドラ
スライドスイッ
チ

起床要求
関数コール

1秒経過イベント
生成タスク

LED4点滅タスク
メイン関数(blink_main.c)

起動・停止要求

タイマタスクサブ関数群(timer_sub.c)イベン
トスイッチイベント
生成タスク

状態取得

点灯・消灯要求
LED２

プッシュスイッチ3.テスト対象(5/7)• 分割後のファイル構成（LED4関連）
タイマタスクサブ関数群(timer_sub.c)

イベント

LED4点滅タスク
メイン関数(blink_main.c)

点滅周期生成関数群(led4_blink_cyclic.c)
イベント
0.25秒周期ハンドラ

1秒周期ハンドラはテスト対象外としました

関数コール

起床要求
1秒周期ハンドラ(cup_timer.c)
点灯・消灯要求

LED1

LED4点滅タスク
サブ関数群(blink_sub.c)

起動・停止要求

点灯・消灯要求

LED4

0.25秒経過イベント
生成タスク

3.テスト対象(6/7)• 次のフォルダにファイル分割後のソースファイル一式があります。
• sample¥RX63N¥cup_timer10 -> NCES TRAINING BOARD用• sample¥stm32¥cup_timer10 -> STM32F401 Nucleo用• テスト対象のファイルは次の7ファイルになります。
¥CUP_TIMER10│ blink_main.c│ blink_sub.c│ base_time_cyclic.c│ led4_blink_cyclic.c│ sw_state_cyclic.c│ timer_main.c│ timer_sub.c3.テスト対象(7/7)• 状態遷移表とフローチャート• タイマタスクとLED4点滅タスクの状態遷移表、及び各モジュールのフローチャートは下記エクセルを参照願います。

フローチャート• ビルドと動作確認• cup_timer10ディレクトリを¥program¥program_asp¥にコピーしてください。
• 「組込みソフトウェア開発技術の基礎」の12.ITRONプログラミング実習編を参考にcup_timer10をビルドし、マイコンボードに書き込んでください。
12.ITRONプログラミング実習編「カップラーメンタイマの実装」と同じ動作となることを確認してください。

4.テスト環境の構築(1/8)• この単体テスト環境は、以下の環境で動作確認しております。
• Windows10 Pro 64ビット• Pleiades All in One Eclipse リリース2021 Ultimate Windows x64 Ultimate Full
Edition https://mergedoc.osdn.jp/#pleiades.html• 上記サイトから、Eclipse Ultimateをダウンロードしてインストールしてください(この単体テスト環境はCDTとPythonを使用するため、Ultimate版をインストールしております)。
• Pythonとgccの環境があれば他の環境でも動作すると推測します。

4.テスト環境の構築(2/8)• unit_testディレクトリを¥program¥program_asp¥cup_timer10にコピーしてください。

4.テスト環境の構築(3/8)• Eclipseを起動• デフォルトの../workspaceを選択し、起動

4.テスト環境の構築(4/8)• プロジェクトエクスプローラ -> 新規 ->プロジェクトを選択

4.テスト環境の構築(5/8)• C++プロジェクトを選択して次へ4.テスト環境の構築(6/8)• プロジェクト名に任意の文字列(下図ではunit_test_stm32)を入力• デフォルト・ロケーション使用のチェックを外し、ロケーションにcuptimer10¥unit_testを指定• Makefileプロジェクト -> 空のプロジェクト, ツールチェーン -> MinGW GCC を選択し、完了

4.テスト環境の構築(7/8)• ユーザ環境変数のPathに次のディレクトリを追加してください。
C:¥pleiades¥eclipse¥mingw¥bin
Eclipseをインストールしたディレクトリを指定

4.テスト環境の構築(8/8)
unit_test¥build¥run_all.batの下から2行目を次のように変更してください。
C:¥pleiades¥python¥3¥python.exe main.py@pauseEclipseをインストールした
ディレクトリを指定

5.テストケース• テストケース数と解説の範囲• 下表の通り40のテストケースを作成しました。全てのテストケースを説明すると膨大になるため、各テストケースに共通する箇所と、テストケースを2つ抜粋して解説します。
テストケー
スの数

テスト対象のCファイル名

テストケースのファイル名

timer_main.c

testTimerMain.cpp

6

timer_sub.c

testTimerSub.cpp

9base_time_cyclic.c

testBaseTimeCyclic.cpp

2

sw_state_cyclic.c

testSwStateCyclic.cpp

6blink_main.c

testBlinkMain.cpp

7

blink_sub.c

testBlinkSub.cpp

8

led4_blink_cyclic.c

testLed4BlinkCyclic.cpp

2

テストケースの合計

40

5.1.各テストケースに共通する箇所の解説(1/11)• テストケース作成の最初の作業として、GoogleTest を使用するためのヘッダgtest.hと、FFFを使用するためのヘッダFFF.hをincludeします。
• FFFを使うためにはDEFINE_FFF_GLOBALSの記述が必要となります。
• テスト対象をextern “C”で丸ごとincludeします。こうすることで、テスト対象のstatic変数やstatic関数の参照・更新が可能となります。

5.1.各テストケースに共通する箇所の解説(2/11)• 関数の実体があるものはテスト対象のCソースファイルに記述された関数のみです。実体のない関数は、FFFで偽装関数宣言します。
戻り値がvoidの場合:FAKE_VOID_FUNC(偽装する関数名,引数の型);
戻り値がある場合:FAKE_VALUE_FUNC(戻り値の型,偽装する関数名,引数の型);

5.1.各テストケースに共通する箇所の解説(3/11)
・FFFで偽装する関数は、テストケース実行前に初期化する。
偽装関数の初期化リストFFF_FAKE_LISTの定義:FAKE(初期化する偽装関数名)

5.1.各テストケースに共通する箇所の解説(4/11)• テストケースの初期化と後処理
SetUpはTEST_Fで記述された各テストケースの実行前の処理、TearDownが各テストケースの実行後の処理となります。
テスト対象、及びテストケースはグローバル変数を使用しています。
よって、マルチスレッド環境下で複数のテストケースが同時実行された場合、グローバル変数のアクセスでテストが破綻する恐れがあります。
複数のテストケースを同時実行することは無いとは思いますが、 保険としてmutexにてテストケースを逐次実行するようにしました。

5.1.各テストケースに共通する箇所の解説(5/11)• テストケースの追加の方法
TEST_F(テストグループ名,テスト名称)でテストケースを追加していきます。
TEST_FはGoogleTestのマクロで、複数のテストケースで同じデータ設定を使う際に使用します。

EXPECT_EQは GoogleTestの判定文で、1つ目の引数に期待値、2つ目の引数に検証対象を記述します。主な判定文を次ページ以降に記載します。

5.1.各テストケースに共通する箇所の解説(6/11)
・GoogleTestの判定文について
GoogleTestでは、判定文を生成するマクロを用いてテストの成功/失敗を判定します。本資料では主な判定文としてTRUE/FALSEの判定と2値の判定のみ掲載しますが、他にも文字列の比較等の判定文があります。詳細はGoogle
Test ドキュメント日本語訳http://opencv.jp/googletestdocs/index.htmlを参照願います。
・判定文の使い分け方
判定文は、ASSERT_*とEXPECT_*の2種類に大別されます。
ASSERT_*は、判定条件が失敗の時、実行中のテストケースを中断し、次のテストケースを実行します。

5.1.各テストケースに共通する箇所の解説(7/11)
EXPECT_*は、判定条件が失敗の時でもテストを中断せず、次のテストを実行します。
通常は1度のテストで複数の失敗を検出できるEXPECT_*が便利です。

・ASSERT_*が役に立つケース①プロジェクトの規定上、1つでもエラーを検出したテストケースはテストのやり直しになる。
②テスト対象のアルゴリズムの性質上、エラー検出後のテストは無意味なものとなる。
①②のどちらもテスト時間が短ければEXPECT*でも支障ないですが、長時間かかる多数のテストケースを一晩中実行する際等に差が出てきます。

5.1.各テストケースに共通する箇所の解説(8/11)• TRUE/FALSEの判定• ASSERT/EXPECT_期待値(引数)の書式で、期待値と引数が異なるとエラーとなります。
判定

判定

判定条件

ASSERT_TRUE(condition);

EXPECT_TRUE(condition);condition が true

ASSERT_FALSE(condition);

EXPECT_FALSE(condition);condition が false

5.1.各テストケースに共通する箇所の解説(9/11)• 2つの値の判定• 1つ目の引数に期待値、2つ目の引数に検証対象を記述します。
判定

判定

判定条件

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

5.1.各テストケースに共通する箇所の解説(10/11)• FFFの主な機能とGoogleTestの判定文の組み合わせによる検証• テスト対象の関数を実行すると、下記の様な値がFFFによって記録されます。
• 渡された引数の確認(Nは引数順): <偽装した関数名>_fake.argN_val• 戻り値の操作:<偽装した関数名>_fake.return_val• 呼び出された回数の確認:<偽装した関数名>_fake.call_count• 関数の呼び出し履歴(Nは呼び出し順):fff.call_history[N]• 複数回呼び出された際の引数の確認 (Nは引数順,Mは呼び出し順):<偽装した関数名>_fake.argN_history[M]• テストの基本的な流れは、テスト対象関数実行後にGoogleTestのASSERT/EXPECT判定文
にて、FFFで記録された上記の値を検証していくことになります。

5.1.各テストケースに共通する箇所の解説(11/11)• カスタムFake関数について• 偽装関数の初期化リストFFF_FAKE_LISTに登録するだけで前ページのFFFの機能を利用することができますが、偽装関数内に任意のロジックを実装したい場合、カスタムFake関数を使います。
• カスタムFake関数:<偽装した関数名>_fake.custom_fake
固定長メモリブロックの獲得を擬似的に実装する
get_mpf_custom_fake関数を作成

テストケース内で、テスト対象のled4_blink_time_task関数
実行前にget_mpf_custom_fake関数をカスタムFake関数に設定

5.2.テストケースの解説(1)スイッチスキャンタスク(1/4)• テスト対象関数 sw_scan_task (関数の詳細は「状態遷移表とフローチャート」を参照願います)
10ms周期ハンドラから起床されると、SW1とPUSH1の状態をスキャンした後、無限ループによって再びスリープする。

5.2.テストケースの解説(1)スイッチスキャンタスク(2/4)• デバイスドライバを偽装して、SW1 とPUSH1の状態を操る
テストケースにて、スイッチの状態を事前に設定する

事前に設定されたスイッチの状態を返す

5.2.テストケースの解説(1)スイッチスキャンタスク(3/4)• SW1 とPUSH1の同時押下のテストケース
前ページの偽装デバイスドライバをカスタムfAKE関数に設定
sw1とPUSH1の現状態をOFFに設定
前ページの偽装されたデバイスドライバがsw1とPUSH1をONを返すように設定
setjmpライブラリ実行後、無限ループを含むテスト対象sw_scan_taskをコール
次ページの偽装slp_tsk内のlongjmpにより無限ループから脱出する
テスト結果の検証
関数の呼び出し履歴(Nは呼び出し順):fff.call_history[N]
渡された引数の確認(Nは引数順): <関数名>_fake.argN_val

5.2.テストケースの解説(1)スイッチスキャンタスク(4/4)• TOPPERSのサービスコール slp_tsk の偽装• テスト対象(無限ループ)からコールされる偽装slp_tskが2回コールされると、longjmpライブラリによって無限ループから脱出し、テストケースに戻る。結果的に、テスト対象はキースキャンを1回実行することになる。
呼び出された回数の確認:<関数名>_fake.call_count5.3.テストケースの解説(2)LED4点滅タスク(1/5)• テスト対象関数 blink_task (関数の詳細は「状態遷移表とフローチャート」を参照願います)• データキュー受信により起床するイベントドリブンなタスク•

データキュー受信の偽装が必要となる

5.3.テストケースの解説(2)LED4点滅タスク(2/5)• TOPPERSサービスコール rcv_dtq の偽装• テスト対象(無限ループ)からコールされる偽装rcv_dtqがテストケースで設定される
loop_numを超えてコールされると、longjmpライブラリによって無限ループから脱出し、テストケースに戻る。
呼び出された回数の確認:<関数名>_fake.call_count

固定長メモリブロックはmalloc/freeで代替する

テストケースで事前に設定したイベントを代入する

5.3.テストケースの解説(2)LED4点滅タスク(3/5)• TOPPERSサービスコール rel_mpfの偽装• 解放するメモリブロックのアドレスを検証する

固定長メモリブロックはmalloc/freeで代替する

5.3.テストケースの解説(2)LED4点滅タスク(4/5)• Blinkタスクの点滅状態(消灯)での全イベント受信のテストケース(1/2)

この配列にEV_BLINKで定義されるメッセー
ジを追加していきます。
要素順にデータキューにメッセージが送信されます。
配列の最大要素数は
TEST_SCENARIO_MAXで調整します。

静的API（ATT_INI）により登録した初期化ルーチンは自動的に実行されない
ため、ここで実行するタスクの状態を保持する変数を書き換える

5.3.テストケースの解説(2)LED4点滅タスク(5/5)• Blinkタスクの点滅状態(消灯)での全イベント受信のテストケース(2/2)
setjmpライブラリ実行後、無限ループを含むテスト対象blink_taskをコール

偽装rcv_dtq内のlongjmpにより無限ループから脱出する

テスト結果の検証
関数の呼び出し履歴(Nは呼び出し順):fff.call_history[N]6.実行ファイルの生成(1/2)• Eclipseを起動• プロジェクトエクスプローラ -> プロジェクト(下図ではunit_test_stm32)を右クリック -> プロジェクトのビルド

6.実行ファイルの生成(2/2)• ビルドが成功すると、cup_timer10¥unit_test¥build¥に、7つのexeファイルが生成されます。

7.テストの自動実行(1/2)• Windowsのエクスプローラから、
cup_timer10¥unit_test¥build¥run_all.batをダブルクリックすると、全テストケースが自動実行されます。

7.テストの自動実行(2/2)• Dosプロンプトが起動し、テスト結果が表示されます。

各テストケー
スの実行ログ

テスト全体の結果

8.テスト結果(1/10)• テスト結果はDosプロンプトに表示されると共に、resultフォルダにxmlとcsvで出力されます。

8. テスト結果(2/10)• タイマメイン

8. テスト結果(3/10)• タイマサブ8.テスト結果(4/10)• ベースタイム周期ハンドラ

8.テスト結果(5/10)• スイッチ8.テスト結果(6/10)• ブリンクメイン

8.テスト結果(7/10)• ブリンクサブ

8.テスト結果(8/10)• LED4点滅周期ハンドラ

8.テスト結果(9/10)• Pythonによる
XML‐CSVパース• GoogleTestが出力したテスト結果
XMLをCSV形式に成形して出力する
プログラム

8.テスト結果(10/10)• テスト結果 概要出力
(0_Total Result.csv)• テストケース 詳細出力
（testSwStateCyclic.csv）

9.テストの応用• 再現性の低いバグの検出
再現性の低いバグは、タスク間メッセージの受信の順番に起因して発生することがよくあります。
例)タスクAからaメッセージ受信(めったに受信しない)
タスクBからbメッセージ受信(同上)タスクCからcメッセージ受信(同上)→この順番で受信した場合のみ不具合発生• このような検証には,全タスクの全メッセージの組合せをテストしなければならないため,テストケースが膨大になります。
• 本書で紹介したテストケースは単純なメッセージIDの1次元配列ですが、この配列を改造することで,複数のタスクからの複数のメッセージの偽装が可能となります。夜仕掛けて朝には数兆通りのメッセージ受信順のテスト結果が出来上がっているといった運用が期待できます。

10.まとめ• デバイス不要の単体テストが実現できた• 実機を使わずに、PC上で単体テストを完結できた• テストの自動化で、網羅性・柔軟性の高いテストが容易に実現できた• 数多いテストケースやシナリオを容易に実行することができた• 実際のデバイス操作では実現しにくいケースも容易にテストすることができた• テストケース作成の知識を得ることができた• テストをする際の考え方などを知ることができた