近年におけるソフトウェア製品の需要拡大をうけた品質や信頼性・安全性の要求を効率的に実現するための手法として、「テスト自動化」が注目されています。しかし、実際に自動化の取り組みを始めても専門書の通りに実施することが難しかったり、現実的な品質とコストの判断に苦戦し期待する効果が得られないことも少なくありません。特に事例に関する情報は少なく、一方で「理想的な解」が広く公開されています。そのため設定するゴールが無意識に高くなってしまい、かえって現場を疲弊させてしまうケースも存在します。

そこで本稿では、無理なくテスト自動化を進めることができたひとつの事例として、弊社が開発しているDTシリーズ製品におけるテスト自動化の取り組みの一部をご紹介したいと思います。

コンセプトを決める

まずは現実的な品質とコストのバランスについて方針を決めておきます。DTシリーズ開発のテスト自動化としては、次のようなコンセプトを考えました。

不具合が致命的な欠陥となりやすい中核の機能を自動テストの対象とする

問題が発生した場合、製品が提供するメインのワークフローが遂行できなくなる機能を、第一のテスト対象とします。

メインのワークフローとは、多くは製品のマニュアルに書かれているような、ユーザーが有益なアウトプットを得るまでの基本的な操作手順です。DTシリーズの場合は、プロジェクト作成 → テストポイント自動挿入 → レポート収集 → レポート解析 の手順です。

これら以外は従来通り、手動テストでカバーすることとしました。

APIレベルのブラックボックステストを行う

将来的に大きな変更が入りづらく、テスタビリティの高い箇所から自動テストを始めることにしました。

モジュールの公開APIは、他のモジュールに対しての「約束」です。特に開発者間、あるいはチーム間で開発を分業している場合は慎重に合意・文書化されているはずです。DTシリーズでもそのようなモジュールがいくつか存在し、これらはAPIの入出力が定められているためテスト基準を作りやすかったため、最初に自動テストの対象としました。

派生開発での自動テスト導入のアプローチとしては最も安定だと思います。

テストシステム自体の複雑度を抑える

テストシステムはそれ自体がひとつのソフトウェアです。これにも更新を重ねるにつれてメンテナンスコストが増えたり、不具合が紛れ込む可能性が増えるといった、通常のソフトウェア同様の課題があります。

面倒を見る必要があるシステムが増えて結果的に工数が増えてしまっては本末転倒ですので、メンテナンスが難しくなりそうなテストは自動化せず、手動テストでカバーすることとしました。

テスト対象を決める

自動挿入機能

テストポイントの自動挿入機能はひとつのライブラリファイルとして独立しており、DTシリーズ製品のコードの中でも最も自動テストしやすいモジュールです。また自動挿入というワークフローの中の非常に重要な部分を担うモジュールですので、自動テストの対象とすることにしました。

DT-WinのドライバDLL

DT-Winでのレポート収集に必要なドライバDLLですが、リンクできるターゲットプロジェクトの種類(フレームワークやアーキテクチャなど)の組み合わせは40種類ほどになります。自動挿入機能と同じく、ドライバDLLもまたDT-Winのメインワークフローを担うモジュールで、同様のテストの繰り返しが非常に多いため、自動テストの対象としました。

外部モジュールの受け入れ

DTシリーズ製品はいくつかの他社システムを、機能の一部として取り込んでいます。こういったシステムの多くはソースコードが公開されていませんので、バージョンアップの際は API に着目したリグレッションテスト(ブラックボックステスト)によって、入出力の破壊的な変更が行われていないことを確認します。

運用ルールを決める

それぞれ、リポジトリにコミットする前に各開発者のローカルPCでテストを実行することとしました。理想的にはリポジトリへのコミットをフックして Jenkins などの CI ツールへ連携したいところですが、ビルド環境の一部に開発環境構築が困難であるモジュールが含まれているため、これについては今後の課題としています。

そのため開発者に負担をかけないようにするため、テスト実行は可能な限り簡単に行えるよう、コマンドひとつで起動できることを重視することにしました。

テストシステムを実装する

自動挿入機能のテスト

このモジュールは C++ のスタティックライブラリですので、テストのたびにテストプログラムへリンクし、実行ファイルをビルドしなければなりません。そこで、 googletestCMakeCTestを使って自動化することにしました。

テストは、テストポイント自動挿入後のファイル内容を、あらかじめ用意されている正解パターンと比較することにより行うこととしました。全体の動作概要は次の図のとおりです。

最も基本的なテストコードは次のようなイメージとなります。

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



lt;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シリーズ製品のテスト自動化についてご紹介しました。

テスト自動化の理論的な解は様々な手法とともに公開されていますが、実用的な解はテスト対象のシステムによって大きく異なります。テスト対象のシステムごとに何が最適なのかを求めなければなりませんが、本稿がそのような活動の参考になれば幸いです。

また、弊社ではお客様のテスト自動化に関するセミナーを開催します。社内のプロセスだけでなくお客様のテスト自動化のためのシステム構築を行った経験から、テスト自動化を始めるためのベストプラクティスなどを詳細に解説いたします。以下のページから申し込みができますので、本稿と併せてぜひお役立てください。

ソフトウェア開発を劇的に変えるテスト自動化ソリューション – Small Start Kit

テストツールの導入 = スタートライン。
はやく使えるようになり、チームで運用できるようにして、成果を上げてこそ、導入のメリットがあります。
ハートランド・データの「Small Start Kit」。
それはお客様が最短距離で成果を上げるためのテストソリューションです。
詳細はこちら