続・1時間でツイッターサービスを作ろうにテストを書いてみた

「偉大なるHelloWorld」をつぶやいている一覧を抽出する
機能追加をしてみます。



rochefort's twitter_helloworld at master - GitHub

1時間でツイッターサービスを作ろうにテストを書いてみた2 - うんたらかんたら日記

1時間でツイッターサービスを作ろうにテストを書いてみた - うんたらかんたら日記

下調べ

twitter-authは基本はget/postのようなので
http://search.twitter.com を使った場合どういう値が返るかconsoleで確認してみます。

a = me.twitter.get('http://search.twitter.com/search.json?q=ruby')

>> pp a
{"max_id"=>26835747788,
 "results"=>
  [{"created_at"=>"Sat, 09 Oct 2010 11:40:15 +0000",
    "profile_image_url"=>
     "http://a1.twimg.com/profile_images/1045704045/linkedin-logo_normal.jpg",
    "from_user"=>"linkedinevents",
    "text"=>
     "New Event: Ruby Social Club Milano. As usual, we’ll have lightning talks and short live coding demos. Anyone is... http://bit.ly/bhJOD6",
    "to_user_id"=>nil,
    "metadata"=>{"result_type"=>"recent"},
    "id"=>26835747788,
    "geo"=>{"coordinates"=>[45.507, 9.2299], "type"=>"Point"},
    "from_user_id"=>130219011,
    "iso_language_code"=>"en",
    "source"=>
     "<a href="http://www.linkedin.com/" rel="nofollow">Linkedin Events</a>",
    "place"=>
     {"id"=>"e6b05bc6fa05d24b",
      "type"=>"neighborhood",
      "full_name"=>"Monza, Milano"}},

略

 "since_id"=>0,
 "refresh_url"=>"?since_id=26835747788&q=ruby",
 "next_page"=>"?page=2&max_id=26835747788&q=ruby",
 "page"=>1,
 "results_per_page"=>15,
 "completed_in"=>0.051111,
 "query"=>"ruby"}

resultsにtweet情報が配列で格納されているのが分かります。
時間と名前を出力するviewを作ってみることにします。


いざテスト

またmodelには修正はないのでcontrollerから。
とりあえず/messages/listにGETした場合、status=200となるようにテストを書いてみます。

#spec/controllers/message_controller_spec.rb
  describe "GET 'list'" do
    it "should be successful" do
      get 'list'
      response.should be_success
    end
  end


エラーとなることを確認し、プロダクトコードを書きます。

#app/models/messages_controller.rb
  def list
  end

#app/views/messages/list.html.erb  を作成。

#config/routes.rb
  map.list 'list', :controller => 'messages', :action => 'list'

これで通りました。


さらにテストを書いていきます

searchの結果を@statusesにセットすることを想定します。

#spec/controllers/message_controller_spec.rb
  describe "GET 'list'" do
    context "偉大なるHelloWorldを検索し、検索結果が0件でない場合" do
      before do
        get_search_stub.and_return(dummy_search)
        get 'list'
      end
      it "should be successful" do
        response.should be_success
      end
      it "@statusesが空でないこと" do
        assigns[:statuses].should_not be_empty
      end
    end
  end

  def post_tweet_stub
    controller.stub_chain(:current_user, :twitter, :post).with('/statuses/update.json', :status => "偉大なるHelloWorld")
  end

  def get_search_stub
    controller.stub_chain(:current_user, :twitter, :get).with("http://search.twitter.com/search.json?q=偉大なるHelloWorld")
  end
  
  def dummy_search
    {"max_id"=>26835747788,
     "results"=>(1..3).map{ |tid| dummy_tweet(tid) }
    }
  end
  
  def dummy_tweet(id)
    {"id"=>id,
      "created_at"=>"Sat, 09 Oct 2010 11:40:15 +0000",
      "profile_image_url"=>"http://example.com/profile_images/pi.png",
      "from_user"=>"username#{id}",
      "text"=>"偉大なるHelloWorld",
      "to_user_id"=>nil,
      "geo"=>nil,
      "from_user_id"=>id,
      "iso_language_code"=>"ja",
      "source"=>"web"}
  end


テストの失敗を確認。

$ spec spec/controllers
.F............


1)
NoMethodError in 'MessagesController GET 'list' 偉大なるHelloWorldを検索し、検索結果が0件でない場合 @statusesが空でないこと'
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.empty?


プロダクトコードを書く。

#app/models/messages_controller.rb
  def list
    @statuses = current_user.twitter.get('http://search.twitter.com/search.json?q=偉大なるHelloWorld')
  end


テストの成功を確認。


テストを修正。

#spec/controllers/message_controller_spec.rb
      it { response.should be_success }
      it { response.should render_template('list') }
      it { assigns[:statuses].should_not be_empty }


red / green / refactoring!!

次はview

spec/views/messages/list.html.erb_spec.rbを作成


viewから使用するため
dummy_search、dummy_tweetをspec_helperへ。
移動後、controllerのテストを実施。問題ないことを確認。

#spec/views/messages/list.html.erb_spec.rb
require 'spec_helper'

describe "/messages/list" do
  before do
    assigns[:satatuses] = dummy_search
    render 'messages/list'
  end

  it "「偉大なるHelloWorld」のつぶやき一覧を表示すること" do
    response.should have_tag('#tweet1', /username1/)
    response.should have_tag('#tweet2', /username2/)
    response.should have_tag('#tweet3', /username3/)
  end
end
#app/views/messages/list.html.erb
<table>
  <tr>
    <th>id</th>
    <th>name</th>
    <th>date</th>
  </tr>
<%- @statuses["results"].each_with_index do |t,i| -%>
  <tr id="tweet<%=i+1%>">
    <td><%= t["id"].to_s %></td>
    <td><%= t["from_user"] %></td>
    <td><%= t["created_at"] %></td>
  </tr>
<%- end -%>
</table>

テストはこれで通ります。


が、実際にscript/serverで動かしてみると
エラーとなります。

URI::InvalidURIError in MessagesController#list

bad URI(is not URI?): http://search.twitter.com/search.json?q=偉大なるHelloWorld

queryに日本語をそのまま渡しているのが原因でした。


url encodeさせるとうまいくいくので、テストも併せて修正。


※url encodeについてまとめ記事かきました。
※ので、CGI.escapeは、ERB::Util.u に置き換えてください。
rubyでurlのencode - うんたらかんたら日記


#app/models/messages_controller.rb
  def list
    @statuses = current_user.twitter.get("http://search.twitter.com/search.json?q=#{CGI.escape '偉大なるHelloWorld'}")
  end

#spec/controllers/message_controller_spec.rb
  def get_search_stub
    controller.stub_chain(:current_user, :twitter, :get).with("http://search.twitter.com/search.json?q=#{CGI.escape '偉大なるHelloWorld'}")
  end

テスト結果

$ spec spec -cfn
MessagesController
  GET 'index'
    should be successful
  POST 'create'
    tweetに成功した場合
      should eql "おめでとう!偉大なるHelloWorldは成功した。"
      should redirect to "/"
    tweetに失敗した場合
      should eql "残念だが、偉大なるHelloWorldは失敗に終わった。"
      should render template "index"
      should be success
    tweet時にduplicateエラーによる例外が発生した場合
      should eql "連投エラーです。"
      should render template "index"
      should == "500"
    tweet時に予期せぬエラーによる例外が発生した場合
      should eql "予期せぬエラーが発生しました。"
      should render template "index"
      should == "500"
  GET 'list'
    偉大なるHelloWorldを検索し、検索結果が0件でない場合
      should be success
      should render template "list"
      should not be empty
MessagesHelper
/messages/index
  ログインしている場合
    「偉大なるHelloWorldをツイートする」ボタンを表示すること
  ログインしていない場合
    ログインリンクを表示すること
/messages/list
  「偉大なるHelloWorld」のつぶやき一覧を表示すること

Finished in 0.169787 seconds

19 examples, 0 failures

やはり、itにまかせた部分のメッセージが気になります。
ここは、subject/itsをうまく使えばクリアに出来ると思いますが
subjectの対象が一連のテストの中で微妙に異なるので
どうしたもんかと思い、とりあえず使用しないままにしています。

あとは

0件の場合とか
indexからlinkさせるとか
エラーハンドリング追加するとかあるとは思うけど
飽きてきたので終了にします。