プログラマのためのテスト2
TRANSCRIPT
UnitTest入門とUnitTestデザインパターン
メソッド単位でコードを検証するテストコードを書き、
戻り値、副作用が妥当であることを確認するテスト
// テスト対象メソッド addition(int arg1, int arg2) // 引数の和を返し、メンバ変数m_lastResultに結果を格納するメソッド
int result = addition(2,3); // テスト対象のメソッドを実行して
CPPUNIT_ASSERT_EQUAL((int) 5, result); // 結果を確認CPPUNIT_ASSERT_EQUAL((int) 5, m_lastResult); // 結果を確認
UnitTest(C++/CPPUnit)のCode例
UnitTestユニットテスト
Green テスト成功 Red テスト失敗
UnitTestの基本方針
• 確実にNGになるケースをまず書こうテストが実行されていることを確かめましょう
例:CPPUNIT_FAIL("message"); /// 必ず失敗し、messageを表示するテストテストできたらCPPUNIT_FAILを外してGreenにします。
• テストを書こう新規実装であれば仕様をコードに落としながら。既存実装の修正1. 既存のコードに合致するテストを書く。2. 修正後の仕様を満たすテストを書く。
• 本番コードを書こうテストがGreenになるように本番コードを書いていきます。テストで仕様抜け、テストコードパス抜けを見つけたらテストを書き足します。
• リファクタリング必要があれば、テストを成功のまま保ちつつリファクタリングを行います。
Green テスト成功Red テスト失敗
Red テスト失敗
Green テスト成功
Green テスト成功
Green テスト成功
テスト基本形
// テスト対象メソッド Calc::addition(int arg1, int arg2) // 引数の和を返し、メンバ変数m_lastResultに結果を格納するメソッドvoid CalcUnitTest::test_Addition(void){
Calc calc; // テスト対象クラスを作成int result = calc.addition(2,3); // テスト対象のメソッドを実行して
CPPUNIT_ASSERT_EQUAL((int) 5, result); // 結果を確認CPPUNIT_ASSERT_EQUAL((int) 5, calc.m_lastResult); // 結果を確認// ↑calc.m_lastResultにテストクラスからアクセスできるように、// テスト対象クラスのヘッダファイルで以下のようにfriend指定しておきます。// friend class FrameBuilderUnitTest;
}
CPPUNIT_ASSERT_EQUAL (x,y) → x==y の場合にOK。CPPUNIT_ASSERT_EQUAL((int) 期待値, テスト対象値);通常、期待値を第1引数に、テスト対象値を第2引数に渡します。型をCPPUnitに伝えるために、明示的にキャストする必要があります。
例外送出のテスト
void YarinageUnitTest::test_Set(void){
Yarinage yarinage; Yari yari;
try{yarinage.Set(yari);
// NGパターンCPPUNIT_FAIL("何も飛んできませんでしたよ。");
} catch (Yari e) {// OKパターン槍が投げられました。
} catch (...){// NGパターンCPPUNIT_FAIL("槍じゃないものが飛んできましたよ。");
}}
または、特別なことをせず意図しないコードパスにCPPUNIT_FAILを入れてNGにすれば良い。
/// テスト対象 Yarinage::Set(Yari yari) /// 槍をセットされると槍型の例外を投げるメソッド。void YarinageUnitTest::test_Set(void){
Yarinage yarinage; // テスト対象クラスを作成Yari yari; // テストに使うクラスを作成
CPPUNIT_ASSERT_THROW(yarinage.Set(yari), Yari);// 飛ばないことを確認するには// CPPUNIT_ASSERT_NO_THROW();
}
CPPUNIT_ASSERT_THROW(実行,送出予期型);CPPUNIT_ASSERT_NO_THROW();
全パス・全状態の網羅
直交表や全ペアといったテスト技法が助けになります→参考文献:「はじめて学ぶソフトウェアのテスト技法」
直行表
Mock(Stub)
C++ヘッダファイルからMockを生成するMockMaker作りました。(C++ヘッダのパースがまだまだ全然ダメですが)http://igarashikuniaki.net/data/MockMaker/MockMaker.zip
制御可能なニセモノ下位モジュール
テスト対象モジュール
PaiCalcGetResult (1000000)
5分計算して結果を返す
PaiCalcMockGetResult (1000000)
てきとーな値ですぐに結果を返す返す値はテストコードから制御可能
テストコードホンモノコード
Calc::Calc(){ // コンストラクタ#ifndef UNITTEST
m_paiCalc = new PaiCalc(); // ホンモノ#else
m_paiCalc = new PaiCalcMock(); // ニセモノ#endif}
メソッド呼び出しの確認
Foo(テスト対象モジュール)
PaiCalcMock
テストコード
g_is_ PaiCalcMock_GetResult _Invoked = false;foo.DoCalcPai(); // テスト実行CPPUNIT_ASSERT_EQUAL((bool)true,
g_is_ PaiCalcMock_GetResult _Invoked);
Foo::DoCalcPai() {m_paiCalc->GetResult (1000000);
// Test時はMockが呼び出される}
PaiCalcMock::GetResult (1000000){g_is_ PaiCalcMock_GetResult _Invoked = true// メソッド内でグローバル変数に値代入
}
テスト対象モジュールFoo::DoCalcPai()内でPaiCalc::GetResult()が呼び出されるか確認するテスト
テスタで導通チェックをするようなものです。
UnitTest実装時のCheckList
• 全てのパスを通過• 全ての環境(変数やメモリ状態)の組み合わせ• 全ての異常系• 全ての状態遷移• 例外送出• NULLチェック• 境界値• 変換 -逆変換 - diff• Initializeが呼ばれる前に全メソッドを呼ぶテスト• Finalizeが呼ばれた後に全メソッドを呼ぶテスト• Initializeを呼んだ後にInitializeを呼ぶテスト
Test First DevelopmentTest Driven Development本番コードを書く前にユニットテストコードを書く→ テストコードに駆動される開発
テストコードで境界値や全てのケースを網羅しようという意識になる
仕様が固まっていない部分は固めようとする
テスト容易な設計を考慮するようになる
テストを満たすように、突貫コードを書く
仕様破綻がないかチェックする
エラーケースをケアするコードを書く
リファクタリング
テスト容易な設計とはなんだ?
Mockオブジェクトをどうやって置き換えるか?
• #ifdef (前述)
• DI(Dependency Injection:依存性の注入)
(例: GoFのストラテジーdesign patternを利用)
Foo::Foo(){ // コンストラクタ#ifndef UNITTEST
m_bar = new Real(); // ホンモノ#else
m_bar = new Mock(); // ニセモノ#endif}
Dependency InjectionでReal/Mock切りかえ
InterfaceFooCalc()純粋仮想関数
RealFooCalc()を実装
MockFooCalc()を実装
RealBarSetFoo(InterfaceFoo* foo){
m_foo = foo;}
m_foo->Calc();// 本番時はRealFoo// テスト時はMockFoo
本番コードRealMain(){
RealBar bar;bar.SetFoo(new RealFoo());...
}
テストコードTestMain(){
RealBar bar;bar.SetFoo(new McokFoo());...
}
本番コードにテストコードが含まれないスッキリした設計・実装!
テスト容易な設計とはなんだ?
• 適切なモジュール化
テストを書きづらいコードはかかない
こんなメソッドは嫌だ
• 仕事内容が多岐
• 1000行
==== DI とMock Object を利用した UnitTest Pattern ====
■本番コードBar class は Foo class の演算結果を利用して処理をします。
class IFoo : public IFoo {int CalcFoo();
}
class RealFoo {int CalcFoo() {return 3;
}}
class RealBar{
IFoo *m_foo;
void SetFoo(IFoo* foo) {m_foo = foo;
}
// CalcFoo() の結果を 2倍するint CalcBar() {return 2 * m_foo->CalcFoo();
}}
void main(){RealFoo foo;RealBar bar;
// Setter Injection. bar.SetFoo(&foo);printf("CalcBar = %d¥n", bar.CalcBar());
}
■テストコード// 実際はMockppを利用して、より柔軟にテスト条件に応じた値を返す Mock を作るclass MockFoo : public IFoo {
int m_calc_value;
void SetCalcFoo(int value) {m_calc_value = value;
}
int CalcFoo() {return m_calc_value;
}}
void test_main(){
MockFoo foo;RealBar bar;
bar.SetFoo(&foo);
foo.SetCalcValue(1);ASSERT(bar.CalcBar() == 2);
foo.SetCalcValue(2);ASSERT(bar.CalcBar() == 4);
}
Mockppを利用することで尐し楽をして Mock class を作成できるようになります。
このようにしてMock Class を一度作っておけば、テスト本体だけでテスト条件を記述できるようになり、テスト時も本番時もまったく同じオブジェクトを利用できるようになります。# リビルド不要、本番とテストでコードの食い違いが起こりえない
また、必ず依存関係の間にインタフェースが挟まるので、それぞれのクラスを別々に開発することも容易になります。# 依存先の実体がなくてもコンパイルできるし、Mock を入れて# 逐次テストができる
つまり、DI + MockObjectで UnitTestというのは、このようなメリットを得るために、本番コードのアーキテクチャを変えるというextreme なテスト手法です。
サンプルコード
(c)ひぐまさん多謝!!
Javaの世界は一歩先を行く
• Seasar2 (DIコンテナFrameWork)
コンテナの中身を外部ファイルで自由に操作.xml
aのインスタンスは○○bのインスタンスは△△cのインスタンスは□□...
DIコンテナ
コード使用したいコンテナ(クラス)の名前だけしっていればいい。
そのインスタンスが何かは意識しなくて良い。
Real.xmlaのインスタンスはRealAbのインスタンスはRealBcのインスタンスはRealC
Mock.xmlaのインスタンスはMockAbのインスタンスはMockBcのインスタンスはMockC
TestA.xmlaのインスタンスはRealAbのインスタンスはMockBcのインスタンスはMockC
本番・テストごとにxmlを切り替えてReal/Mockを指定する。
UnitTestPatterns
どのようなUnitTestを書け
ばいいのかを形式化し、テストファーストをプログラマの習慣にすることを目指すwebページ
http://www.marcclifton.com/tabid/87/Default.aspx (英語)
http://igarashikuniaki.net/fswiki/wiki.cgi?page=UnitTestPatterns
(日本語翻訳途中)
テストしやすい設計をするために、テストファースト。テストを設計前に書けば、テストまで考慮した設計が可能。
■The Simple-Test Pattern(シンプルテストパターン)■The Code-Path Pattern(コードパスパターン)■The Parameter-Range Pattern(パラメータレンジパターン)■Data Driven Test Patterns(データ駆動テストパターン)■The Simple-Test-Data Pattern(シンプルテストデータパターン)■The Data-Transformation-Test Pattern(データ変換テストパターン)■Data Transaction Patterns (データトランザクションパターン)■The Simple-Data-I/O Pattern(シンプルデータI/Oパターン)■The Constraint-Data Pattern(制約データパターン)■The Rollback Pattern(ロールバックパターン)■Collection Management Patterns(コレクション管理パターン)■The Collection-Order Pattern(コレクションオーダリングパターン)■The Enumeration Pattern(列挙パターン)■The Collection-Constraint Pattern(コレクション制約パターン)■The Collection-Indexing Pattern(コレクションインデクシングパターン)■Performance Patterns(パフォーマンスパターン)■The Performance-Test Pattern(パフォーマンステストパターン)■Process Patterns(プロセスパターン)■The Process-Sequence Pattern(プロセスシーケンスパターン)■The Process-State Pattern(プロセス状態パターン)■The Process-Rule Pattern(プロセスルールパターン)■Simulation Patterns(シミュレーションパターン)■Mock-Object Pattern(モックオブジェクトパターン)■The Service-Simulation Pattern(サービスシミュレーションパターン)■The Bit-Error-Simulation Pattern(ビットエラーシミュレーションパターン)
UnitTestPatterns
テストを育てる
テストが書かれている範囲はコントロール下にあるということ。
テストを追加していくということは、テストにより動作保証される範囲を増やし、リスク把握できる部分を広げていくこと。
OK
OK
OK
バグの場所が分かるドラゴンレーダーが欲しいです。
現在、テストがカバーしているコードがどの辺なのか見える化してくれるツールがあるといいですね。
※NGでひっかかった回数
を記録するような仕組みがあると面白いね。
UnitTestツール
xUnit•JUnit (JAVA)•NUnit (C#)•CppUnit (C++)
どの環境でもユニットテストツールは大抵あります。
CppUnit動作環境VC++, g++(GCC)など。
UnitTestのまとめ
• テストにより駆動される開発
–気づけなかったものに気づく
–発想を広げる
• テスト容易性まで考慮した設計
–モジュール化重要
• テストを用いたリスク解析テスト△はOKなので、△の範囲は問題無し
テスト□がNGだったので、□の範囲内に問題有り
参考文献
• はじめて学ぶソフトウェアのテスト技法著:リー・コープランド ISBN : 4-8222-8251-1これ1冊でプログラマレベルではテストマスター
• CPPUnitによる実践テスト技法http://www.mikamama.com/CppUnitBook/draft/index.html (draft)
著:大月美佳 ISBN:4-7980-0571-1ユニットテストの入門書。リファレンスとしても便利。
• 知識ゼロから学ぶソフトウェアテスト著:高橋寿一 ISBN:4-7981-0709-3SONYのテストエンジニアさんの著書。実際の経験に基づくノウハウが多数。
• WEB+DB PRESS vol.35著:和田卓人 ISBN:4-7741-2931-3Java DIコンテナSeasar2を使ったユニットテストの記事など入門記事。
おしまい
質問があればお願いします。
テストのゴールはいつだ?
テストにかかった費用+
サポートにかかる費用+
メンテナンスにかかる費用
が最小値になるとき