ifstreamをモック化
gmockを使えば他クラス実装への依存を減らせるので有り難いのですが、今回のお題であるifstreamのように非virtual・既存・変更不可なクラスをモック化したい場合にはちょっとしたトリックが必要です。
トリックの内容は公式のクックブックの通りですが、以下の3点です:
- モック化したいクラスを継承するのではなく、別クラスとして定義する。
- 下記の例ではifstreamの継承ではなくMockStreamを作成。
- テスト対象クラスをテンプレートクラスとする。テンプレートパラメータとしてモック化対象クラスを指定する。
- 例ではBinaryReaderクラスをテンプレート化。テンプレートパラメータとして、プロダクトコードではifstreamを、テストコードではMockStreamを指定。
- モック化対象クラスを参照ではなくポインタで保持する。
以下のサンプルは、ifstreamを使ってバイナリデータを読むためのクラスと、そのテストコードです。
#include <gtest/gtest.h> #include <gmock/gmock.h> using namespace ::testing; class MockStream { public: MOCK_METHOD2(read, MockStream&(char* s, int n)); }; template<class T> class BinaryReader { T* const m_stream; public: BinaryReader(T& stream) : m_stream(&stream) {} template<typename U> void read(U& data) { m_stream->read(reinterpret_cast<char*>(&data), sizeof(data)); } }; TEST(BinaryReader, read_char) { StrictMock<MockStream> stream; EXPECT_CALL(stream, read(A<char*>(), TypedEq<int>(1))) .Times(1) .WillOnce(DoAll(SetArgPointee<0>(0x01), ReturnRef(stream))); BinaryReader<MockStream> reader(stream); char data; reader.read(data); ASSERT_EQ(char(0x01), data); }
モック対象クラスをT*とポインタで保持してある点について補足を。
ポインタは気持ち悪いので参照で保持しておきたいところです。こんな感じですね。
template<class T> class BinaryReader { T& m_stream; public: BinaryReader(T& stream) : m_stream(stream) {} };
これをコンパイルするにはモッククラスにコピーコンストラクタを定義する必要があります。
しかしEXPECT_CALLが見ているのはstreamであってコピー先のインスタンス(BinaryReader::m_stream)とは別物なので、モックがうまく動きません。
結果としてポインタで持たざるを得ないということになります。
…この結論に辿り着くまで随分時間がかかってしまったので、以下のようにモックを使わず直接ファイルを読むテストを書く方が早いと思ってしまいました :p
TEST(BinaryReader, read_char) {
std::ifstream stream("test.dat");
BinaryReader reader(stream);
char data;
reader.read(data);
ASSERT_EQ(char(0x41), data);
}
誰かの時間節約に役立ちますように(未来の自分を含む)。
PHPUnitサンプル
PHPでユニットテストを書くのはこんなに簡単、というサンプル。
題材は黄表紙『達人プログラマー』より。
■テストコード:largestTest.php
<?php require_once("largest.php"); class largestTest extends PHPUnit_Framework_TestCase { public function testOne() { $this->assertEquals(1, largest(array(1))); } public function testDups() { $this->assertEquals(9, largest(array(9, 7, 8, 9))); } public function testOrder() { $this->assertEquals(9, largest(array(9, 8, 7))); $this->assertEquals(9, largest(array(7, 9, 8))); $this->assertEquals(9, largest(array(7, 8, 9))); } public function testNegative() { $this->assertEquals(-7, largest(array(-9, -8, -7))); } public function testEmpty() { try { largest(array()); die("ここへは来ない"); } catch (Exception $e) { $this->assertTrue(TRUE); } } } ?>
■プロダクトコード:largest.php
<?php define('PHP_INT_MIN', ~PHP_INT_MAX); function largest($list) { if (count($list) == 0) { throw new Exception("空のリストが渡された"); } $max = PHP_INT_MIN; foreach ($list as $i) { if ($max < $i) { $max = $i; } } return $max; } ?>
この本、内容が古くなっているだけに絶版なのかな。ユニットテストの章は現役だと思うが…

達人プログラマー―ソフトウェア開発に不可欠な基礎知識 バージョン管理/ユニットテスト/自動化 (Ascii software engineering series)
- 作者: デビッドトーマス,マイククラーク,アンドリューハント,David Thomas,Mike Clark,Andrew Hunt,長瀬嘉秀,テクノロジックアート
- 出版社/メーカー: アスキー
- 発売日: 2005/03
- メディア: 単行本
- 購入: 3人 クリック: 91回
- この商品を含むブログ (70件) を見る
JSONスキーマで妥当性確認
JSONスキーマでJSONファイルをvalidateするだけのコード。
#!/usr/bin/env ruby require 'rubygems' require 'json-schema' exit 1 unless ARGV.length == 2 schema = ARGV[0] json = ARGV[1] errors = JSON::Validator.fully_validate(schema, json) errors.each do |error| puts error end
スキーマの書き方に自信をもてないので、上記スクリプトに .../gems/json-schema-1.0.9/resources/draft-03.json (JSON Schema自体のスキーマ定義)とスキーマファイルを指定してvalidateしながら書いている。
【情報まとめ】WebAPIの仕様化やテストについて
ドキュメンテーション
テスト記述
- Test Your API with Cucumber and json_spec
- Testing Web APIs with Ruby and Cucumber
- 書籍
- 『モダンPerl入門』- 6.3:Apacheとテスト
- 『Cucumber Recipes』- Recipe36:Monitoring a Web Service Using Nagios and Cucumber

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)
- 作者: 山本陽平
- 出版社/メーカー: 技術評論社
- 発売日: 2010/04/08
- メディア: 単行本(ソフトカバー)
- 購入: 138人 クリック: 4,194回
- この商品を含むブログ (159件) を見る

サービスデザインパターン SOAP/WSDLとRESTful Webサービスの基本的な設計ソリューション
- 作者: Robert Daigneau,角征典,高木正弘
- 出版社/メーカー: アスキー・メディアワークス
- 発売日: 2012/08/24
- メディア: 大型本
- 購入: 1人 クリック: 34回
- この商品を含むブログを見る

- 作者: 牧大輔
- 出版社/メーカー: 翔泳社
- 発売日: 2009/02/10
- メディア: 大型本
- 購入: 24人 クリック: 498回
- この商品を含むブログ (113件) を見る

Cucumber Recipes: Automate Anything With Bdd Tools and Techniques
- 作者: Ian Dees,Matt Wynne,Aslak Hellesoy
- 出版社/メーカー: Pragmatic Bookshelf
- 発売日: 2013/03/07
- メディア: ペーパーバック
- この商品を含むブログを見る
Nokogiri+CucumberにてXMLの妥当性確認
XMLに対応したXSDを用意し、それをNokogiriで読んでvalidateする。
その結果をCucumberで受ける。
…というだけのものです。
■features/validate_xml.feature
機能: XSDによりXMLの妥当性を確認する シナリオ: XMLとスキーマを読んで妥当性を確認 前提: XMLドキュメント "sample1.xml" を読む かつ: スキーマ "sample1.xsd" を読む もし: ドキュメントがスキーマに準拠している ならば: ドキュメントは妥当性確認された
■features/step_definitions/xml_validation.rb
前提 /^: XMLドキュメント "(.*?)" を読む$/ do |fname| @doc = Nokogiri::XML(File.read(fname)) end 前提 /^: スキーマ "(.*?)" を読む$/ do |fname| @xsd = Nokogiri::XML::Schema(File.read(fname)) end もし /^: ドキュメントがスキーマに準拠している$/ do @valid = true @xsd.validate(@doc).each do |error| @valid = false puts "XML not schema valid ([#{error.line}-#{error.column}] #{error.message})" end end ならば /^: ドキュメントは妥当性確認された$/ do @valid.should be_true end
■features/support/env.rb
require 'nokogiri'
PlantUMLでアクティビティ図を描く
"IT専科TOP > UML入門 > アクティビティ図(Activity Diagram)"の最後にある例をPlantUMLで作ってみた。
試行錯誤したものの完全には再現できなかった。

「サーバー」を先に定義することでHTTPとFTPをサーバーのパーティションに属させつつ、クライアント側にifを入れることで分岐をクライアントのパーティションに属させることはできた。
パーティションを綺麗に並べる方法はわからない。
流れを横方向にする方法もわからない。
(-->を->や-right->に変えてみたが、良い結果ではなかった。Graphvizならrankdir=LRなのだが)
ソース:
partition サーバー {
HTTP --> (*)
FTP --> (*)
}
partition クライアント {
(*) --> 選択
選択 if "" then
--> HTTP
else
--> FTP
endif
}
XMLから指定ノードを削除するパッチ
XMLファイルにノード削除パッチをあてたいので、Ruby + Nokogiriで書いてみた。
# -*- coding: utf-8 -*- require 'rspec' require 'nokogiri' class XmlPatch def initialize(xml) @doc = Nokogiri.XML(xml) end def del_node(tag_name, attr_name, attr_val) counter = 0 @doc.root.xpath("//#{tag_name}").each { |node| if node.has_attribute?(attr_name) and node.attr(attr_name) == attr_val then node.remove counter += 1 end } counter end def to_s @doc.to_s end end describe 'XmlPatchクラス' do describe 'コンストラクタ' do it 'xml属性を設定する' do patch = XmlPatch.new('<A><B><C tag="TAG"/></B></A>') patch.to_s.should == "<?xml version=\"1.0\"?>\n<A>\n <B>\n <C tag=\"TAG\"/>\n </B>\n</A>\n" end end describe 'del_node_by_id' do it '指定タグのうち指定idを持つノードを削除し、削除した件数を返却する' do patch = XmlPatch.new('<A><B><C id="01234"/><C id="56789"/></B><C id="01234"/></A>') patch.del_node('C', 'id', '01234').should == 2 patch.to_s.should == "<?xml version=\"1.0\"?>\n<A>\n <B>\n <C id=\"56789\"/>\n </B>\n</A>\n" end end end