Ruby Freaks Lounge

第37回実用的なダミーサーバ ww(double-web)(2)

前回第35回はwwを使ってWebのダブルとなるサーバを作り、スパイ機能を使ってクライアントからのリクエストの状況を目視確認する方法を説明しました。

今回は、ミニブログへのメッセージ投稿を通じて、wwを自動化テストに組み込む方法を説明します。

RSpecの自動テストの中からサーバを起動停止する

wwは、単一のサーバプロセスとして起動させるほかに、自動化テストの中で定義・起動・停止するためのAPIを備えています。前回作ったダブルサーバを、RSpecから起動・停止するテストコードは次のようになります。

# spec/miniblog_client_spec.rb

$:.unshift File.expand_path("../lib", File.dirname(__FILE__))
require 'miniblog_client'
require 'ww'

describe MiniblogClient do
  before(:all) do
    WW::Server.handler = :webrick
    WW::Server[:miniblog] ||= WW::Server.build_double(3080) do
      m_offset = 0

      spy.get("/messages/:user.json") do |user|
        content_type "application/json"
        t = Time.local(2010, 3, 13, 12, 34 + (m_offset += 1), 56)
        body = [
          {:message => "#{user}です。ミニブログ始めました", :posted_on => t.iso8601},
          {:message => "2つめの#{user}つぶやきです", :posted_on => (t + 10*60).iso8601},
        ].to_json
      end
    end
    WW::Server.start_once(:miniblog)
  end

  it "サーバが正常に起動していること" do
    expect {
      TCPSocket.open('localhost', 3080).close
    }.should_not raise_exception(Errno::ECONNREFUSED)
  end
end

アクションを定義しているブロックは、前回のWW::SpyEyeを使った場合とまったく同じになります。 WW::Server.build_dobule(port)は引数で指定されたポートで起動するダブルサーバを定義します。つまり、この例では、前回作ったのと同じダブルサーバを3080番ポートで起動するように定義しています。また、二重起動防止やテスト中からの操作のために、 WW::Server.[]=(identifier, server)メソッドでwwが用意しているコンテナに入れています。

サーバを起動させるには、WW::Server.start_once(identifier)メソッドを呼び出します。このメソッドは、指定したidentifierのダブルサーバが起動していない場合は起動させ、すでに起動している場合はスパイなどの呼び出し情報をリセットします。また、ここで起動したサーバは、スクリプトの終了時に自動的に終了します[1]⁠。

サーバへのリクエストをモックする

では、メッセージの投稿機能を実装しましょう。サーバ側のWebAPIは「投稿ユーザ名とメッセージ内容を、/messagesにPOSTする」というものとします。認証機能などは、いまは考えません。

リクエストの有無をモックで検証する

wwでは2種類の方法でモックを定義できます。

一つ目はWW::Server.build_double()のブロック内でmock()メソッドを使い、あらかじめ定義する方法です。これはすべてのテストケースで有効にしたいモック(たとえば、ログイン機能など)を定義するのに便利です。もう一つは、 WW::Server.mock(identifier)で後から定義する方法です。こちらは、必要となるモックをテストケースごとに定義できます。

今回はpost_entryに関するテストケースでのみ使いたいため、後者の方法でモックを定義します。指定したサーバへのモック呼び出しの有無は、after { } ブロックでのWW::Server.verify(identifier)で検証します。

diff --git a/spec/miniblog_client_spec.rb b/spec/miniblog_client_spec.rb
@@ -24,4 +24,22 @@ describe MiniblogClient do
      expect { TCPSocket.open("localhost", 3080).close }.
       should_not raise_exception(Errno::ECONNREFUSED)
   end
+
+ describe "#post_entry" do
+   before do
+     WW::Server.mock(:miniblog).post("/messages") do
+       ""
+     end
+     conn = Connection.new("localhost", 3080)
+     @client = MiniblogClient.new(conn)
+   end
+
+   after do
+     WW::Server.verify(:miniblog)
+   end
+
+   it "/messageにメッセージをPOSTすること" do
+     @client.post_entry("moro", "こんばんは")
+   end
+ end
 end

まずは、モックの役割の一つである、⁠呼び出されなかった場合にその旨を通知する」機能を試すため、MiniblogClient#post_entry()を空のメソッドとして定義し、テストを実行してみます。

# lib/miniblog_client.rb (※ ソースのディレクトリを、作業ルートからlibに移しています)
diff --git a/lib/miniblog_client.rb b/lib/miniblog_client.rb
@@ -18,6 +18,9 @@ class MiniblogClient

     newer_first ? entries.reverse : entries
   end
+
+  def post_entry(*args)
+  end
 end

 class Connection

この状態でテストを実行します。モックしている、つまり呼び出しを期待しているアクションが呼ばれないため、テストが失敗します。

$ spec -fn -c spec/miniblog_client_spec.rb

MiniblogClient
  サーバが正常に起動していること
  #post_entry
    /messageにメッセージをPOSTすること (FAILED - 1)

1)
Ww::Double::MockError in 'MiniblogClient #post_entry /messageにメッセージをPOSTすること'
  Ww::Double::MockError
/Users/moro/tmp/gihyo.jp_ww/spec/miniblog_client_spec.rb:38:

Finished in 0.284092 seconds

2 examples, 1 failure

予想通りテストが失敗することを確認できたので、MiniblogClient#post_entryを実装します。

diff --git a/lib/miniblog_client.rb b/lib/miniblog_client.rb
@@ -1,6 +1,8 @@
 require 'rubygems'
 require 'json'
 require 'open-uri'
+require 'net/http'
+require 'rack/utils'

 class MiniblogClient
   def initialize(conn, *friends)
@@ -17,7 +19,8 @@ class MiniblogClient
     newer_first ? entries.reverse : entries
   end

- def post_entry(*args)
+ def post_entry(name, message)
+   @connection.post("/messages", "user" => name, "message" => message)
  end
 end

@@ -28,6 +31,14 @@ class Connection
 def get(abs_path)
  open("http://#{@host}:#{@port}#{abs_path}").read

  end
+
+ def post(abs_path, data)
+   req = Net::HTTP::Post.new(abs_path)
+   req.body = data.map { |k,v|
+     "#{Rack::Utils.escape(k.to_s)}=#{Rack::Utils.escape(v.to_s)}"
+   }.join("&")
+   Net::HTTP.start(@host, @port) { |http| http.request(req) }
+ end
 end

 if __FILE__ == $0

これでテストが通るようになりました。試しに実行してみましょう。

$ spec -fn -c spec/miniblog_client_spec.rb

MiniblogClient
  サーバが正常に起動していること
  #post_entry
    /messageにメッセージをPOSTすること

Finished in 0.345286 seconds

2 examples, 0 failures

期待するリクエストの内容つきでモックする

さらに、いま述べたようなリクエストの有無だけを検証するのではなく、リクエストの内容が期待するものかどうかも検証できます。リクエストの内容を検証するには、mock()メソッドの第二引数として期待するパラメータ名と値をHashで渡します。

たとえば、送信されるmessageが「こんばんは」であることを検証するには、次のように定義します。

diff --git a/spec/miniblog_client_spec.rb b/spec/miniblog_client_spec.rb
@@ -27,7 +27,7 @@ describe MiniblogClient do

  describe "#post_entry" do
    before do
-   WW::Server.mock(:miniblog).post("/messages") do
+   WW::Server.mock(:miniblog, "message" => "こんばんは").post("/messages") do
     ""

   end
   conn = Connection.new("localhost", 3080)

こうすると、@client.post_entry("moro", "こんばんは")の場合にはテストが成功し、 @client.post_entry("moro", "こんにちは")と呼び出した場合にはテストが失敗するようになります。"user"パラメータについては特に検証しないため、@client.post_entry("morohashi", "こんばんは")などという呼び出しでも テストは成功します。リクエストの中で特に検証したいパラメータのみを指定するとよいでしょう。

スパイのリクエストの状況を検証する

前回紹介した、スパイの機能も使えます。

たとえば、前回は目視確認した「MiniblogClient#entry_listで、3回(friendsの数だけ)/messages/:users.jsonにアクセスしていること」を自動化テスト内で検証するには、次のようなテストを書きます。

diff --git a/spec/miniblog_client_spec.rb b/spec/miniblog_client_spec.rb
@@ -42,4 +42,15 @@ describe MiniblogClient do
      @client.post_entry("moro", "こんばんは")
    end
  end
+
+ describe "#entry_list" do
+   before do
+     conn = Connection.new("localhost", 3080)
+     client = MiniblogClient.new(conn, "alice", "bob", "charls")
+
+     client.entry_list
+   end
+   subject { WW::Server[:miniblog] }
+   it { should have(3).requests }
+ end
 end

スパイとして定義したアクションへのリクエストは、WW::Server#requestsメソッドで取得できます。 例では回数だけを検証していますが、このリクエストオブジェクトからHTTPメソッドやパス、クエリパラメータなどを取得して検証できます。ただし、あくまでスパイですので、モックのように自動的に検証したりはしません。スパイしたリクエストを取得し、この例のshould have(3).requestsのように自分で検証する必要があります。

このテストを実行すると、次のようにパスします。

$ spec -fn -c spec/miniblog_client_spec.rb

MiniblogClient
  サーバが正常に起動していること
  #post_entry
    /messageにメッセージをPOSTすること
  #entry_list
    should have 3 requests

Finished in 0.197325 seconds

3 examples, 0 failures

モックとスパイを使い分ける判断基準ですが、筆者の感触では、サーバ側のAPIを含めて設計していたり、そのAPIを始めて使ったりと「自分が作っているものがハッキリしない」場合はスパイから取得したリクエストをもとに試行錯誤するほうがよいと感じています。その後、ある程度動くものができてからは、モックを使ってテストすべきことを明確にすると、意図を表現できているよいテストになりそうです。

おわりに

今回は、wwを自動化テストから使い、Webサーバとの間で行われているインタラクションを検証する方法を説明しました。

wwは現在も開発中であり、機能のリクエストやバグ報告、よりよいAPIの提案なども募集中です。アイディアがありましたらhttp://github.com/moro/ww/issuesやTwitterの@moroなどで、いろいろお聞かせいただければ嬉しく思います。

外部サービスとの連携は、アプリケーション開発の中でも特に難しく感じることが多い場面です。wwが少しでもその不安を取り除き、楽しくテストを書くために役立てればうれしく思います。

おすすめ記事

記事・ニュース一覧