近年におけるソフトウェア製品の需要拡大をうけた品質や信頼性・安全性の要求を効率的に実現するための手法として、「テスト自動化」が注目されています。しかし、実際に自動化の取り組みを始めても専門書の通りに実施することが難しかったり、現実的な品質とコストの判断に苦戦し期待する効果が得られないことも少なくありません。特に事例に関する情報は少なく、一方で「理想的な解」が広く公開されています。そのため設定するゴールが無意識に高くなってしまい、かえって現場を疲弊させてしまうケースも存在します。
そこで本稿では、無理なくテスト自動化を進めることができたひとつの事例として、弊社が開発しているDTシリーズ製品におけるテスト自動化の取り組みの一部をご紹介したいと思います。
コンセプトを決める
まずは現実的な品質とコストのバランスについて方針を決めておきます。DTシリーズ開発のテスト自動化としては、次のようなコンセプトを考えました。
不具合が致命的な欠陥となりやすい中核の機能を自動テストの対象とする
問題が発生した場合、製品が提供するメインのワークフローが遂行できなくなる機能を、第一のテスト対象とします。
メインのワークフローとは、多くは製品のマニュアルに書かれているような、ユーザーが有益なアウトプットを得るまでの基本的な操作手順です。DTシリーズの場合は、プロジェクト作成 → テストポイント自動挿入 → レポート収集 → レポート解析 の手順です。
これら以外は従来通り、手動テストでカバーすることとしました。
APIレベルのブラックボックステストを行う
将来的に大きな変更が入りづらく、テスタビリティの高い箇所から自動テストを始めることにしました。
モジュールの公開APIは、他のモジュールに対しての「約束」です。特に開発者間、あるいはチーム間で開発を分業している場合は慎重に合意・文書化されているはずです。DTシリーズでもそのようなモジュールがいくつか存在し、これらはAPIの入出力が定められているためテスト基準を作りやすかったため、最初に自動テストの対象としました。
派生開発での自動テスト導入のアプローチとしては最も安定だと思います。
テストシステム自体の複雑度を抑える
テストシステムはそれ自体がひとつのソフトウェアです。これにも更新を重ねるにつれてメンテナンスコストが増えたり、不具合が紛れ込む可能性が増えるといった、通常のソフトウェア同様の課題があります。
面倒を見る必要があるシステムが増えて結果的に工数が増えてしまっては本末転倒ですので、メンテナンスが難しくなりそうなテストは自動化せず、手動テストでカバーすることとしました。
テスト対象を決める
自動挿入機能
テストポイントの自動挿入機能はひとつのライブラリファイルとして独立しており、DTシリーズ製品のコードの中でも最も自動テストしやすいモジュールです。また自動挿入というワークフローの中の非常に重要な部分を担うモジュールですので、自動テストの対象とすることにしました。
DT-WinのドライバDLL
DT-Winでのレポート収集に必要なドライバDLLですが、リンクできるターゲットプロジェクトの種類(フレームワークやアーキテクチャなど)の組み合わせは40種類ほどになります。自動挿入機能と同じく、ドライバDLLもまたDT-Winのメインワークフローを担うモジュールで、同様のテストの繰り返しが非常に多いため、自動テストの対象としました。
外部モジュールの受け入れ
DTシリーズ製品はいくつかの他社システムを、機能の一部として取り込んでいます。こういったシステムの多くはソースコードが公開されていませんので、バージョンアップの際は API に着目したリグレッションテスト(ブラックボックステスト)によって、入出力の破壊的な変更が行われていないことを確認します。
運用ルールを決める
それぞれ、リポジトリにコミットする前に各開発者のローカルPCでテストを実行することとしました。理想的にはリポジトリへのコミットをフックして Jenkins などの CI ツールへ連携したいところですが、ビルド環境の一部に開発環境構築が困難であるモジュールが含まれているため、これについては今後の課題としています。
そのため開発者に負担をかけないようにするため、テスト実行は可能な限り簡単に行えるよう、コマンドひとつで起動できることを重視することにしました。
テストシステムを実装する
自動挿入機能のテスト
このモジュールは C++ のスタティックライブラリですので、テストのたびにテストプログラムへリンクし、実行ファイルをビルドしなければなりません。そこで、 googletest・CMake・CTestを使って自動化することにしました。
テストは、テストポイント自動挿入後のファイル内容を、あらかじめ用意されている正解パターンと比較することにより行うこととしました。全体の動作概要は次の図のとおりです。
最も基本的なテストコードは次のようなイメージとなります。
TEST_F(AutoInsertTest, CppCode) { AutoInsertSettings settings; settings.inputSourceFile = "data/code.cpp"; // 入力ソースファイル settings.outputSourceFile = "actual/code.cpp"; // 出力ソースファイル settings.outputHeaderFile = "actual/code.h"; // 出力ヘッダファイル AutoInsert a; bool result = a.InsertTestPoint(settings); EXPECT_TRUE(result); // 関数の成否 EXPECT_TRUE(EqualFiles("actual/code.cpp", "expect/code.cpp")); // ソースファイルの内容比較 EXPECT_TRUE(EqualFiles("actual/code.h", "expect/code.h")); // ヘッダファイルの内容比較 }
テストプログラムをビルドするための CMakeLists.txt は次のようなイメージとなります。
# 自動挿入機能 (AutoInsert.lib) をリンクした実行ファイルを作成する add_executable(AutoInsertTest path/to/gtest-all.cc テストコード.cpp) target_link_libraries(AutoInsertTest AutoInsert.lib) # CTest で実行できるようにする add_test(NAME AutoInsertTest_Test COMMAND
<TARGET_FILE:AutoInsertTest>
テストの実行は、コマンドプロンプトから CMakeLists.txt を保存したフォルダで次のコマンドを実行します。
ctest -C Release
実行結果は次のように報告されます。(テストに失敗した場合)
... [ RUN ] AutoInsertTest.CppCode AutoInsertTest_Cpp.cpp(32): error: Value of: EXPECT_TRUE(result); Actual: false Expected: true 50% tests passed, 1 tests failed out of 2 Total Test time (real) = 8.31 sec The following tests FAILED: 2 - AutoInsertTest_Test (Failed) Errors while running CTest
DT-WinのドライバDLLのテスト
ドライバDLLは多くの種類のDLLを ZIP ファイルとしてまとめて、ユーザーへ公開する最終成果物としています。ドライバDLLのテストでは、この ZIP ファイルを入力としたデータ駆動型のテストプログラムを自作しました。
与えられた ZIP ファイルに含まれる DLL を列挙し、例えば次のようなマトリクスを生成してテストを行います。(実際はプログラミング言語、アーキテクチャ、フレームワーク、ドライバDLLの種別などをすべて掛け合わせているため非常に大きなマトリクスとなっています…)
テストポイントトレース | メモリトレース | パフォーマンステスト | |
C++,x86,SingleThread | 〇 | 〇 | 〇 |
C#,x86,.NET3.5,SingleThread | 〇 | × | 〇 |
それぞれ、単純にレポート取得できること、メモリトレースができること、オーバーヘッドが要求以内に収まっていることを確認します。
テストの実行は、コマンドプロンプトから次のようなコマンドを入力して開始するようにしました。
DriverDLL_RegressionTestTool <ZIPファイル>
テスト結果は次のように報告されます。
... ---------------------- TargetDLL: C_CPP | WriteLock | x64 TPTrace: OK MemoryTrace: OK TPOverHead: OK ---------------------- TargetDLL: C_CPP | WriteLock | x86 TPTrace: OK MemoryTrace: OK TPOverHead: OK ---------------------- 18件のテストを実施しました。 ===== OK: 18, NG: 0, Skip: 0 =====
外部モジュールの受け入れテスト
DTシリーズと連携している製品はいくつかありますが、ここでは例としてコマンドラインツールをテストしてみます。ここでのテストはコマンドラインツールを引数付きで呼び出し、標準出力された値が正しいかを検証します。
プロセスコールや文字列解析は C++ よりも C# の方が得意であることが多いため、ここでは xUnit を使用してテストすることとしました。
最も基本的なテストコードは次のようなイメージとなります。
[Fact] public void Test1() { using (var p = new Process()) { p.StartInfo.FileName = "<テスト対象のEXE>"; p.StartInfo.Arguments = "<コマンドライン引数>"; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.Start(); var str = p.StandardOutput.ReadToEnd(); Assert.Contains("XXXX", str); // 標準出力に XXXX という文字が含まれているか? } }
テストの実行は、コマンドプロンプトからソリューションファイル (.sln) を保存したフォルダで次のコマンドを実行します。
dotnet test
テスト結果は次のように報告されます。(テストに失敗した場合)
合計 1 個のテスト ファイルが指定されたパターンと一致しました。 [xUnit.net 00:00:00.65] UnitTest.UnitTest1.Test1 [FAIL] X UnitTest.UnitTest1.Test1 [8ms] エラー メッセージ: ... スタック トレース: ... テストの実行に失敗しました。 テストの合計数: 1 失敗: 1 合計時間: 1.5810 秒
現在の運用状況
「自動挿入機能」及び「ドライバDLL」については計画通り、各開発者のPCで運用が続けられています。
また今後の改善として、以前はこれらのテストターゲットのビルド環境の都合で実現の難しかった、リポジトリへのコミットをフックして CI ツールで実行する計画を進めています。
一方「外部モジュールの受け入れ」は、特に他社様と共同作業のようなスタイルで新機能の開発を進めている間、毎日のように更新が入ってくるモジュールの受入テストで活躍していました。現在は対象モジュールが安定していることもあり、積極的には運用されていません。
開発にかけた時間に対するコスト的な効果は、DTシリーズのリリースを3回ほど経てようやく出てきたといったところです。しかしなによりもデグレードしていないことの保証が “いつでも”・”ヒューマンエラー無く” できるようになったことで、品質の改善につなげることができました。
おわりに
今回は「テスト自動化の事例」という視点で、一部ではありますがDTシリーズ製品のテスト自動化についてご紹介しました。
テスト自動化の理論的な解は様々な手法とともに公開されていますが、実用的な解はテスト対象のシステムによって大きく異なります。テスト対象のシステムごとに何が最適なのかを求めなければなりませんが、本稿がそのような活動の参考になれば幸いです。
また、弊社ではお客様のテスト自動化に関するセミナーを開催します。社内のプロセスだけでなくお客様のテスト自動化のためのシステム構築を行った経験から、テスト自動化を始めるためのベストプラクティスなどを詳細に解説いたします。以下のページから申し込みができますので、本稿と併せてぜひお役立てください。
【好評御礼!無料のウェビナー!】
近年の組込み機器開発の現場では「テスト自動化」が進んでいます。
しかし、いざ自動化に取り組もうとしても、
「具体的な実現イメージが湧かない・・・」
「テストケースの作成段階では作る人によってバラつきが出てしまう・・・」
「ターゲットごとのテスト環境の構築が大変・・・」
といった課題をお持ちの方は多いのではないでしょうか?
そこで今回のセミナーでは、
お手持ちのラズパイを使って「かんたんに」「低コストに」テスト自動化環境の構築を実現できる
「AUTOmeal(オートミール)」をご紹介します!