はじめに
Web APIを使って様々なサービスと連携するというアーキテクチャはすっかり定着した感があります。みなさんも、
Web APIを使うアプリケーションの開発では、
筆者も、
ダミーWebサーバ作成ライブラリww(Double Web)
wwは、
- 軽量WebサービスフレームワークSinatra(原悠さんによるRuby Freaks Loungeでの紹介記事) を使って簡単なWebサービスAPIを作成できる。 
- 実際にWebサーバとして動作する。アプリケーション側でHTTPライブラリに細工する必要がない。
- 対象となる外部サービスとのやりとりを目視で確認できる。
- 事前に期待するリクエストを定義したり(モック)、 呼び出し後にリクエストを検証したり (スパイ) できる。 
モック? スパイ? ダブル?
開発中やデバッグの際に、
この
- ダブル(Double) 
- モックやスタブ、ダミー、 スパイなど、 実際の環境から切り離してテスト可能にするための手法の総称です。wwの名前も 「WebのDouble」 ということでここからとっています。 
- スタブ(Stub) 
- 自動化テストからの呼び出しを単純に置き換えるための手法です。実際に呼び出されたときに、固定値など簡単な応答をします。呼ばれ方の検証などには関知しません。 
- モック(Mock) 
- 自動化テストとコンポーネント間で、相互の呼び出しを検証するための手法です。呼ばれ方や、 呼ばれる順番などを検証します。また、 モックを定義するということは 「そのテストで呼び出される」 はずであることを意味します。そのため、 一度も呼び出されていない場合にその旨を通知することもあります。 
- スパイ(Spy) 
- 自動化テストから呼び出されたことを事後に検証するための情報をとっておく手法です。簡単な応答をしたあとに、実際の呼ばれ方も検証したい場合に使います。 
より詳しい情報は、
 
実際にwwを使ってみる
それでは、
- ユーザごとのエントリをJSON形式で取得できる。
- JSON形式で自分のエントリを投稿できる。
このサービスの
その上で、
- 指定した複数ユーザのエントリを集約し、時系列に表示する。 
- エントリ本文だけを指定すると、JSON形式でミニブログに投稿する。 
とても簡単ですが、
ユーザごとのエントリをJSON形式で取得する
まずは、
$ gem install ww json
ユーザごとにエントリの一覧をJSONで返す 機能をwwで実装すると次のようになります。
# miniblog.ru
require 'rubygems'
require 'json'
require 'ww'
app = WW::SpyEye.to_app do
  min_offset = 0
  get("/messages/:user.json") do |user|
    content_type "application/json"
    t = Time.local(2010, 3, 13, 12, 34 + (min_offset += 1), 56)
    body = [
      {:message => "#{user}です。ミニブログ始めました", :posted_on => t.iso8601},
      {:message => "2つめの#{user}つぶやきです", :posted_on => (t + 10*60).iso8601},
    ].to_json
  end
end
run appWW::SpyEyeは、
正しく定義できているかを確認するため、
$ rackup -p 3080 miniblog.ru
起動後にcurlコマンドなどでアクセスすると、
$ curl http://localhost:3080/messages/moro.json
[{"message":"moro\u3067\u3059\u3002\u30df\u30cb\u30d6\u30ed\u30b0\u59cb\u3081\u307e\u3057\u305f",
"posted_on":"2010-03-13T12:35:56+09:00"},
{"message":"2\u3064\u3081\u306emoro\u3064\u3076\u3084\u304d\u3067\u3059",
"posted_on":"2010-03-13T12:45:56+09:00"}]
※見やすくするため改行を入れています。
クライアント側のコードは次のようになります。
# miniblog_client.rb
require 'rubygems'
require 'json'
require 'open-uri'
class MiniblogClient
  def initialize(conn, *friends)
    @connection = conn
    @friends = friends
  end
  def entry_list(newer_first = true)
    entries = @friends.
    map {|f| JSON.parse(@connection.get("/messages/#{f}.json")) }.
flatten.
    sort_by {|e| e["posted_on"] }
    newer_first ? entries.reverse : entries
  end
end
class Connection
  def initialize(host, port)
    @host, @port = host, port
  end
  def get(abs_path)
    open("http://#{@host}:#{@port}#{abs_path}").read
  end
end
if __FILE__ == $0
  conn = Connection.new("localhost", 3080)
  client = MiniblogClient.new(conn, "alice", "bob", "charls")
  puts "新着順"
  client.entry_list.each{|e| puts "#{e["posted_on"]}: #{e["message"]}" }
  puts "古い順"
client.entry_list(false).each{|e| puts "#{e["posted_on"]}: #{e["message"]}" }
end実行すると、
$ ruby miniblog_client.rb 新着順 2010-03-13T13:05:56+09:00: 2つめのcharlsつぶやきです 2010-03-13T13:04:56+09:00: 2つめのbobつぶやきです 2010-03-13T13:03:56+09:00: 2つめのaliceつぶやきです 2010-03-13T12:55:56+09:00: charlsです。ミニブログ始めました 2010-03-13T12:54:56+09:00: bobです。ミニブログ始めました 2010-03-13T12:53:56+09:00: aliceです。ミニブログ始めました 古い順 2010-03-13T12:56:56+09:00: aliceです。ミニブログ始めました 2010-03-13T12:57:56+09:00: bobです。ミニブログ始めました 2010-03-13T12:58:56+09:00: charlsです。ミニブログ始めました 2010-03-13T13:06:56+09:00: 2つめのaliceつぶやきです 2010-03-13T13:07:56+09:00: 2つめのbobつぶやきです 2010-03-13T13:08:56+09:00: 2つめのcharlsつぶやきです
リクエストをスパイする
エントリの取得はうまく実装できていそうですが、
スパイ機能を有効にするには、
diff --git a/miniblog.ru b/miniblog.ru
index a88d294..24940cf 100644
--- a/miniblog.ru
+++ b/miniblog.ru
@@ -7,7 +7,7 @@ require 'ww/spy_eye'
app = Ww::SpyEye.to_app do
m_offset = 0
- get("/messages/:user.json") do |user|
+ spy.get("/messages/:user.json") do |user|
content_type "application/json"
t = Time.local(2010, 3, 13, 12, 34 + (m_offset += 1), 56)
body = [ サーバを^Cで停止させたうえで修正し、
再起動したばかりなので、
 
先ほどのクライアントを走らせたあとで確認すると、
 
さらに、
 
このように、
まとめ
今回は、
