mockpp チュートリアル 1.11.4 目次 ・問題提起 ・解決法1:基本的なモックオブジェクト ・解決法2a:visitable モックオブジェクトとマクロ ・解決法2b:visitable モックメソッド ・解決法3a:chainable モックオブジェクトとメソッド ・解決法3b:chainable モックメソッド ・解決法4:Poor Man's モックオブジェクト ・解決法5:Poor Man's モックオブジェクト、第2版 ・CppUnit の基本 ・検証テストケースを利用する 問題提起 mockpp の各要素を紹介するため、単純なテスティングの問題について色々な方法で解決を試みる。 まずは、設定ファイルを読み込む Consumer クラスがある。 この設定ファイルには各行にプレーンテキストでデータレコードが書かれている。 それぞれの行は処理が行われた後で、最後に形式を変更されて書き戻される。 問題の単純化のため、オリジナルレコードに「/processed」を追加することにしよう。 データの処理中には、不定の値に対して calculate メソッドが呼び出される。 Consumer クラスは、純粋仮想クラスである Interface から派生したクラスメソッドを利用してデータの読み込み、書き込みなどを行うこととする。 class Interface { public: virtual void open(const std::string &name) = 0; virtual std::string read() = 0; virtual unsigned calculate(unsigned input) = 0; virtual void write(const std::string &data) = 0; virtual void close() = 0; }; これは二つ以上のクラスが相互に関係し合うという、一般的なテストで起こり得る状況である。 この例では Consumer クラスが想定通りの動作を行うかどうかモックオブジェクトを利用してテストする。 入力ファイルを準備し、ソースコードの変更ごとに出力を手動で確認するとしたら、それはとても退屈な作業だろう。 モックオブジェクトは実際の製品コードとは異なり、テストの時のみ利用される。 理想的には製品コードと同じクラスから派生されるべきである。 次のセクションでは mockpp を利用して開発する際の最も重要な要素を紹介する。 解決法1:Mock Object の基本 最初の解決策は、基本的なモックオブジェクトを利用して要求事項をさっさと満たす方法である。 ここではコンテナを利用して、小さなモックオブジェクトたちを管理、検証する。 このコンテナは Inteface クラスと共に、モックオブジェクトの根源となる MockObject クラスから派生させなければならない。 class BasicMock : public Interface, public MockObject {...} 続いて、詳細部分の初期化を行うコンストラクタを記述する。 全ての(モック)オブジェクトはエラーが起きた時に詳細な情報を出力するため、意味の通る名前を識別子として持つべきである。 これらのオブジェクトはコンテナとなるモックオブジェクトに格納される。 次の例のように、コンテナに格納されるサブオブジェクトに対して段階的な名前付け規則を採用するのは良い考えである。 BasicMock() : MockObject("BasicMock", 0) , open_name("BasicMock/open_name", this) , close_counter("BasicMock/close_counter", this) , write_data("BasicMock/write_data", this) , read_data("BasicMock/read_data", this) { } これらのメソッドの呼び出しは期待通りであるかどうか逐一チェックされる。 作成者の好みに応じて、public なメンバ変数を使うことも、getter や setter を利用するようにすることもできる。 open() 関数の呼び出しチェックには ExpectationList が用いられる。 この方法では、引数が期待される順序で呼ばれたかどうかが検証される。 副作用として、open() 関数の総呼び出し回数も期待した通りか検証される。 ExpectationList open_name; virtual void open(const std::string &name) { open_name.addActual(name); } read() メソッドの実装では違うアプローチを行う。 この場合は、あらかじめ設定しておいた返り値を返すために ReturnObjectList を用いる。 このときも先刻と同様に、副作用として呼び出し回数のチェックが行われる。 ReturnObjectList read_data; virtual std::string read() { return read_data.nextReturnObject(); } 三つ目は close() の呼び出し回数の確認である。 この場合は引数も返り値も無いので、利用するのは ExpectationCounter で十分となる。 ExpectationCounter close_counter; virtual void close() { close_counter.inc(); } モックオブジェクトが実装されたら、次は期待する動作を設定する。 今回の予定では open() の呼び出しが2回(読み込みと書き込みに各1回)だ。 どちらも同じファイル名を使う。 残念ながら、open() の呼び出しが読み書きのどちらのものかを区別する簡単な方法は無い。 そのため、2回の open() 呼び出しパラメータを設定する。 BasicMock mock; mock.open_name.addExpected("file1.lst"); mock.open_name.addExpected("file1.lst"); 次に、正しい回数の read() 返り値設定を行う。 これらは ReturnObjectList にそのままの順序で記憶される。 mock.read_data.addObjectToReturn("record-1"); mock.read_data.addObjectToReturn("record-2"); mock.read_data.addObjectToReturn("record-3"); レコードの処理中に calculate メソッドが何度か呼ばれる。 ここでは入力を厳密に決定することができないため、ExpectationList の代わりとして、(平均値とそこからの差を用いる?) ConstrainList が利用される。 mock.calculate_input.addExpected(eq(5,5)); mock.calculate_input.addExpected(eq(5,5)); mock.calculate_input.addExpected(eq(5,5)); close() の呼び出し回数は open() と同様に 2回である。 このため、ExpectationCounter を 2に設定する。 mock.close_counter.setExpected(2); 最後に write() の期待値を設定して確認用コードの記述は終了となる。 期待されるパラメータを順序通りに設定する。 mock.write_data.addExpected("record-1/processed"); mock.write_data.addExpected("record-2/processed"); mock.write_data.addExpected("record-3/processed"); これでモックオブジェクトへ期待値を設定する作業は終了である。 テスト下にあるメソッドを製品と同様の順序で呼び出す。 もし期待と異なる動作があった場合、例外としてスローされるので、必要に応じて処理すること。 Consumer consumer(&mock); consumer.load(); consumer.process(); consumer.save(); } catch(std::exception &ex) { std::cout << std::endl << "Error occured.\n" << ex.what() << std::endl << std::endl; } 最後に重要な項目が一つある。 なぜなら、期待されるテスト項目が完了されたかどうかを自動的に確認するのは難しいからである。 このため、テスト対象のメソッド呼び出しが完了したことを手動で教えてやる必要がある。 モックオブジェクトコンテナの verify() 関数呼び出しがそれである。 これを記述することにより、内部オブジェクトへテストの完了を通知できる。 mock.verify(); basicmock.cpp(原文ではここにリンクが張ってある) にこれらの完全なソースコードがある。 解決法2a:Visitable モックオブジェクトとマクロ 前の解法では細かい期待値まで手動での調整が必要だった。 mockpp では、そのようなコードの記述量を減らすために、より進んだモックオブジェクトを用意している。 その一つが Visitable アプローチである。 この名前は期待値の設定方法から命名された。 はじめにコンテナとなるモックオブジェクトを実装する。 この点は Interface と同時に VisitableMockObject を継承する以外は以前と同様である。 各機能は内部の変数とヘルパーマクロによって覆い隠される。 以下はコンストラクタで初期化される、各メソッド用の変数群である。 これらはマクロとして働く。 マクロの名前はメソッドの型と引数によって決定される。 より詳しくはハンドブックを参照してほしい。 class VisitMock : public Interface , public VisitableMockObject { public: VisitMock() : VisitableMockObject("VisitMock", 0) , MOCKPP_CONSTRUCT_MEMBERS_FOR_VOID_VISITABLE_EXT1(open, ext) , MOCKPP_CONSTRUCT_MEMBERS_FOR_VISITABLE0(read) , MOCKPP_CONSTRUCT_MEMBERS_FOR_VOID_VISITABLE_EXT1(write, ext) , MOCKPP_CONSTRUCT_MEMBERS_FOR_VOID_VISITABLE0(close) {} ... これでメソッドが実装された。 以降の変数やヘルパーメソッドについてもマクロで処理する。 例えば一番単純なケースではクラス名とメソッド名をマクロに与えるだけでよい。 そのため、close() に関する定義は次のようなもので済む。 MOCKPP_VOID_VISITABLE0(VisitMock, close); もしメソッドが mockpp 標準の形式と異なる場合にはマクロの拡張形式を利用する。 これはオーバーロードされたメソッドについても同様である。 これらはメソッド名やパラメータから内部変数用の名前を導くために必要となる。 より詳しくはハンドブックのセクションを参照してほしい。 MOCKPP_VOID_VISITABLE_EXT1(VisitMock, open, const std::string &, ext, std::string); クラス定義を完了し、使うためには期待される動作の定義も必要である。 このアプローチでは期待されるパラメータでメソッドを呼ぶことで定義を行う。 例えば、ファイルを開き3行読み込んでから閉じる場合はこうなる。 VisitMock mock; mock.open("file1.lst"); mock.read(); mock.read(); mock.read(); mock.close(); この時、read() 関数が返すべき値を準備するためにヘルパーマクロを利用して補助オブジェクトを使う必要がある。 次のように期待される順序でモックオブジェクトへ登録するのである。 MOCKPP_CONTROLLER_FOR(VisitMock, read) read_controller (&mock); read_controller.addReturnValue("record-1"); read_controller.addReturnValue("record-2"); read_controller.addReturnValue("record-3"); 動作登録が終了したら、モックオブジェクトをテストモードにするため activate() を呼び出す。 それからテスト対象となるメソッドを走らせればよい。 前と同様にテストが完了したら verify() を呼んでテストの終了を明示すること。 mock.activate(); Consumer consumer(&mock); consumer.load(); consumer.process(); consumer.save(); mock.verify(); } catch(std::exception &ex) { std::cout << std::endl << "Error occured.\n" << ex.what() << std::endl << std::endl; } (リンク)visitmock.cpp に完全なソースを置いておく。 より高度な機能 Visitable モックオブジェクトはもっと高機能である。 例えば add() というパラメータ二つを取って合計を返すメソッドを考えよう。 これをエミュレートするために、次のような期待値設定を行うことができる。 想定外のパラメータが指定された場合の返り値として -1 を返すように設定する。 これは setDefaultThrowable() を利用することで例外を投げるようにすることもできる。 MOCKPP_CONTROLLER_FOR(VisitMock, add) add_controller (&mock); add_controller.addResponseValue(3, 1, 2); // 1 and 2 are expected add_controller.addResponseValue(110, 99, 11); // 99 and 11 are expected add_controller.setDefaultReturnValue(-1); その他の一般的な問題として、実行時エラーのシミュレートがある。 これらのエラーはしばしば予想しない時に出るが、自分で起こすのは難しい。 このような時に使えるのが Throwable である。 次のコードはネットワーク接続からのバイト読み出しをエミュレートするものである。 最初の呼び出し(パラメータとして10が渡される)は0を返すが、次の呼び出しでは NetworkError をスローする。 その後の呼び出しは全て 1を返す。 class NetworkError {}; MOCKPP_CONTROLLER_FOR(VisitMock, network_read) read_controller (&mock); read_controller.addReturnValue(0, 10); read_controller.addThrowable(make_throwable(NetworkError())); read_controller.setDefaultValue(1); このような振舞いを利用する場合には、ハンドブックの「VisitableMockObject」のセクションを参照し、その詳細を理解しておく必要がある。 ここで、calculate() メソッドに渡されるパラメータだが、不明なりにそれなりの信頼度で実装しなければならない。 これは制約条件を指定することで解決できる。 このケースでは IsCloseTo を利用して偏差のある値を設定する。 mock.calculate(eq(5,5)); mock.calculate(eq(5,5)); mock.calculate(eq(5,5)); 他の制約条件としては、書き戻されるデータの検証がある。 この重要な部分には部分文字列として"processed"が追加されるため、この部分のみを検証する。 mock.write(stringContains(std::string("processed"))); mock.write(stringContains(std::string("processed"))); これらと同様に、返り値を設定すべき場所でも固定値ではなく制約条件を利用することができる。 calculate_controller.addResponseValue(10, eq(2,2)); calculate_controller.addResponseValue(20, eq(4,2)); calculate_controller.addResponseValue(30, eq(6,2)); 完全なソースコードは (リンク)VisitMock.cpp にある。