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


1時間でツイッターサービスを作ろう! | KRAY Inc
これをやってみたついでにRSpecでテストを書いてみました。
herokuももちろん試してみましたが、ここではテストに絞って書いてみたいと思います。
herokuもよさそうですね。


rochefort's twitter_helloworld at master - GitHub

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

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

どう書いて行くか考える

機能を分割すると
機能1:loginしている場合、message/index に「偉大なるHelloWorldをツイートする」buttonを表示させる
    loginしていない場合、ログインへのリンクを表示させる
機能2:button押下時に、twitterへmessageをポストする


まずは、機能1をやってみます。


準備

Railsアプリを作る

Rails2.3.9で作ってしまいました。

$ rails twitter_helloworld
$ cd twitter_helloworld
$ git init
$ script/plugin install git://github.com/mbleigh/twitter-auth.git
$ script/generate twitter_auth
$ rm public/index.html

config/twitter_auth.yml
ここは直書きでもいいですが、
YAMLにrubyのコードを書く方法 - うんたらかんたら日記
を使ってみます。
(※heroku使うなら、直書。)

$ vim config/initializers/env.rb 
ENV['OAUTH_CONSUMER_KEY'] = ""
ENV['OAUTH_CONSUMER_SECRET'] = ""

$ vim config/twitter_auth.yml 
  oauth_consumer_key: <%= ENV['OAUTH_CONSUMER_KEY'] %>
  oauth_consumer_secret: <%= ENV['OAUTH_CONSUMER_SECRET'] %>
$ vim .gitignore
tmp/pidsなどはherokuでは不要なため、-fでgitに追加はしていない。
(localと相違があるからやっておいた方がいいのかもしれない)
.DS_Store
db/*.sqlite3
log/*.log
tmp/**/*
config/initilizers/env.rb


rspecの準備をします。

$ script/generate rspec

一旦commit。

$ git add .
$ git commit -m 'initial'

テストを書いていきます

コントローラ

通常modelからtestを書いて行くところですが
今回model部での特別な実装はないため(User < TwitterAuth::GenericUser)
controllerから書いてみます。


本来的にはテスト書いてからプロダクトコードの実装に移りますが
generate rspec_ とやるとテストファイルも作ってくれるので
generateを使います。

$ script/generate rspec_controller messages index
$ rake db:migrate

自動生成されたテストが通るか確認します。

$ rake spec
....

Finished in 0.132948 seconds

4 examples, 0 failures


spec/controllers/messages_controller_spec.rb を見ると
機能1のcontrollerのテストは自動生成されたテストで十分そうなので
特に追記はしていません(be_an_instance_of は消したので、examplesの数は3)。

ビュー

機能1のviewのテストを書いてみます。
loginしているかどうかというは一旦置いておいて、
「偉大なるHelloWorldをツイートする」ボタンが表示されている
ことを確認するテストを書いてみます。


spec/views/messages/index.html.erb_spec.rb
既存のテストは削除し、下記を追記。

#spec/views/messages/index.html.erb_spec.rb
  it "「偉大なるHelloWorldをツイートする」ボタンを表示すること" do
    response.should have_tag('input', :type => 'submit', :value => '偉大なるHelloWorldをツイートする')
  end


余談ですが、こんな書き方もできるそうです。

    response.should have_tag "form[action=/messages]" do
      with_tag "input[type=submit][value='偉大なるHelloWorldをツイートする']"
      with_tag ...
    end


テストが失敗するのを確認します。

F..


1)
'/messages/index 「偉大なるHelloWorldをツイートする」ボタンを表示すること' FAILED
Expected at least 1 element matching "input", found 0.
<false> is not true.
/Users/rochefort/work/rails/twitter_helloworld/spec/views/messages/index.html.erb_spec.rb:9:

Finished in 0.099799 seconds

3 examples, 1 failure


viewを修正します。

#app/views/messages/index.html.erb
<% form_tag messages_path do %>
  <%= submit_tag '偉大なるHelloWorldをツイートする' %>
<% end %>


テストしてみるとroutingのエラーとなりました。

F..

1)
ActionView::TemplateError in '/messages/index 「偉大なるHelloWorldをツイートする」ボタンを表示すること'
undefined local variable or method `messages_path' for #<ActionView::Base:0x103667958>


なのでroutes,rbを修正。
(createもすぐに作成するので、併せて修正)

$ vim config/routes.rb
  map.resources :messages,:only => [:index, :create]
  map.root :controller => 'messages', :action => 'index'


$ rake routes
(in /Users/rochefort/work/rails/twitter_helloworld)
         login        /login                  {:action=>"new", :controller=>"sessions"}
        logout        /logout                 {:action=>"destroy", :controller=>"sessions"}
   new_session GET    /session/new(.:format)  {:action=>"new", :controller=>"sessions"}
  edit_session GET    /session/edit(.:format) {:action=>"edit", :controller=>"sessions"}
       session GET    /session(.:format)      {:action=>"show", :controller=>"sessions"}
               PUT    /session(.:format)      {:action=>"update", :controller=>"sessions"}
               DELETE /session(.:format)      {:action=>"destroy", :controller=>"sessions"}
               POST   /session(.:format)      {:action=>"create", :controller=>"sessions"}
oauth_callback        /oauth_callback         {:action=>"oauth_callback", :controller=>"sessions"}
      messages GET    /messages(.:format)     {:action=>"index", :controller=>"messages"}
               POST   /messages(.:format)     {:action=>"create", :controller=>"messages"}
          root        /                       {:action=>"index", :controller=>"messages"}

テストが通りました。

..

Finished in 0.095787 seconds

3 examples, 0 failures


一旦commit。

続いてログインの有無

機能1のログインしている場合、していない場合の実装をしてみましよう。
ログイン判定はプラグインの機能なのでstubを使います。
(stubでいいと思ってるけどmockでもいいのか?)

#app/controllers/messages_controller.rb
  it "ログインしている場合、「偉大なるHelloWorldをツイートする」ボタンを表示すること" do
    template.stub(:logged_in?).and_return(true)
    response.should have_tag('input', :type => 'submit', :value => '偉大なるHelloWorldをツイートする')
  end

プロダクトコードは修正しなくてもテストが通ります。


続いて、ログインしていない場合のテストを書いてみます。
重複ロジックはテストが通ってから排除することにします。

#spec/controllers/messages_controller_spec.rb
describe "/messages/index" do
  before(:each) do
    template.stub(:logged_in?).and_return(true)
    render 'messages/index'
  end

  it "ログインしている場合、「偉大なるHelloWorldをツイートする」ボタンを表示すること" do
    response.should have_tag('input', :type => 'submit', :value => '偉大なるHelloWorldをツイートする')
  end
end

describe "/messages/index" do
  before(:each) do
    template.stub(:logged_in?).and_return(false)
    render 'messages/index'
  end

  it "ログインしていない場合ログインリンクを表示すること" do
    response.should have_tag('a', :href => '/login')
  end
end


エラーになります。

...F

1)
'/messages/index ログインしていない場合ログインリンクを表示すること' FAILED
Expected at least 1 element matching "a", found 0.
<false> is not true.
./spec/views/messages/index.html.erb_spec.rb:16:

Finished in 0.167695 seconds

4 examples, 1 failure


viewを修正します。

#app/views/messages/index.html.erb
<% if logged_in? %>
  <% form_tag messages_path do %>
    <%= submit_tag '偉大なるHelloWorldをツイートする' %>
  <% end %>
<% else %>
  <%= link_to 'ログインする', login_path %>
<% end %>

テストは無事通りました。

テストをリファクタリング

ここでテストをリファクタリングします。
こうなりました。

#spec/controllers/messages_controller_spec.rb
describe "/messages/index" do
  context "ログインしている場合" do  
    before do
      template.stub(:logged_in?).and_return(true)
      render 'messages/index'
    end

    it "「偉大なるHelloWorldをツイートする」ボタンを表示すること" do
      response.should have_tag('input', :type => 'submit', :value => '偉大なるHelloWorldをツイートする')
    end
  end

  context "ログインしていない場合" do
    before do
      template.stub(:logged_in?).and_return(false)
      render 'messages/index'
    end

    it "ログインリンクを表示すること" do
      response.should have_tag('a', :href => '/login')
    end
  end
end
$ spec spec/views/ -cfn
/messages/index
  ログインしている場合
    「偉大なるHelloWorldをツイートする」ボタンを表示すること
  ログインしていない場合
    ログインリンクを表示すること

Finished in 0.144496 seconds

2 examples, 0 failures

機能1の完了。8888。



個人的な収穫としては

・template.stub
YAMLにrubyのコードを書く方法 - うんたらかんたら日記
RSpecのstubとstub! - うんたらかんたら日記


勉強になりました。partialのテストも書いてみたいところ。
機能2は明日。テストコードはあとでgithubにでも置きます。
間違い等あればご指摘いただけると助かります。