WebMockでAPIリクエストをstub化する

テスト時に、APIへのリクエストをstub化する用途で使ってみました。
bblimke/webmock


githubのページを見ればわかりますが、
rspec /test unit /minitest/cukeに対応など、とにかく高機能です。
他にも、fakeweb、wwなどがあるようですが、一番更新されてそうだったので
今回はこちらを選択。

使い方

詳細は、githubのページ見てください。
gem入れて、spec_helperでrequireするだけです。

require 'webmock/rspec'


あとは、stub_requestで自由自在。

stub_request(:get, "https://rubygems.org/api/v1/search.json?query=factory_girl").
  to_return(:status => 200, :body => dummy_search_result)

to_returnのところは、ファイルもいけるようです。

他にも

今回は使ってませんが、マッチャも用意されているみたいです。

a_request(:post, "www.example.com").with(:body => "abc", :headers => {'Content-Length' => 3}).should have_been_made.once

a_request(:post, "www.something.com").should have_been_made.times(3)

a_request(:any, "www.example.com").should_not have_been_made

a_request(:post, "www.example.com").with { |req| req.body == "abc" }.should have_been_made

a_request(:get, "www.example.com").with(:query => {"a" => ["b", "c"]}).should have_been_made

a_request(:get, "www.example.com").with(:query => hash_including({"a" => ["b", "c"]})).should have_been_made

a_request(:post, "www.example.com").
  with(:body => {"a" => ["b", "c"]}, :headers => {'Content-Type' => 'application/json'}).should have_been_made

Rails3.1.0でspork


rspecが遅いので、Rails3.1.0でsporkを使おうと思いましたが
エラーとなりました。

$ spork –bootstrap
NOTE: Gem.latest_load_paths is deprecated with no replacement. It will be removed on or after 2011-10-01.
Gem.latest_load_paths called from /Users/rochefort/.rvm/gems/ruby-1.9.2-p290/gems/spork-0.8.5/lib/spork.rb:112.
NOTE: Gem.all_partials is deprecated with no replacement. It will be removed on or after 2011-10-01.
Gem.all_partials called from /Users/rochefort/.rvm/rubies/ruby-1.9.2-p290/lib/ruby/site_ruby/1.9.1/rubygems.rb:598.
NOTE: Gem.all_partials is deprecated with no replacement. It will be removed on or after 2011-10-01.
Gem.all_partials called from /Users/rochefort/.rvm/rubies/ruby-1.9.2-p290/lib/ruby/site_ruby/1.9.1/rubygems.rb:598.
uninitialized constant Spork::Runner::StringIO (NameError)
/Users/rochefort/.rvm/gems/ruby-1.9.2-p290/gems/spork-0.8.5/lib/spork/runner.rb:38:in `supported_test_frameworks_text'
/Users/rochefort/.rvm/gems/ruby-1.9.2-p290/gems/spork-0.8.5/lib/spork/runner.rb:52:in `rescue in find_test_framework'
/Users/rochefort/.rvm/gems/ruby-1.9.2-p290/gems/spork-0.8.5/lib/spork/runner.rb:48:in `find_test_framework'
/Users/rochefort/.rvm/gems/ruby-1.9.2-p290/gems/spork-0.8.5/lib/spork/runner.rb:56:in `run'
/Users/rochefort/.rvm/gems/ruby-1.9.2-p290/gems/spork-0.8.5/lib/spork/runner.rb:9:in `run'
/Users/rochefort/.rvm/gems/ruby-1.9.2-p290/gems/spork-0.8.5/bin/spork:10:in `<top (required)>'
/Users/rochefort/.rvm/gems/ruby-1.9.2-p290/bin/spork:19:in `load'
/Users/rochefort/.rvm/gems/ruby-1.9.2-p290/bin/spork:19:in `<main>'

調べてみると

spork fails to work with rubygems 1.8.0/1.8.1 but works with 1.7.2 / Problems / Discussion Area - RubyGems Support
0.9.0rc7で対応してくれているそうです。
(追記:readme にも書いてました。)


Gemfileを書換えて

gem 'spork', '~> 0.9.0.rc'

bundle installするとうまくいきました。

あとは

1. spork --bootstrap
2. spec_helper修正
1でspec_helperが書き変わるので
今までの内容を、Spork.preforkブロックの中に移行。
3. spork起動
4. rspecをdrbで実行

rspec --drb

とても快適。次はguard。

rspecでrougingのテスト

調べてみると、route_for、route_to というマッチャがあるようで
現状どちらも動きます。
ですが、route_toを使いましょう、というのが結論。

route_for

ここみるとroute_for/params_forが紹介されています。
RSpec.info: Controllers

route_for(:controller => "hello", :action => "world").should == "/hello/world"
params_from(:get, "/hello/world").should == {:controller => "hello", :action => "world"}

route_to

Upgrade.rdocを見るとroute_for/params_fromは今日から使うな。と書いてあります。
Upgrade.rdoc at master from dchelimsky/rspec-rails - GitHub

    { :put => "/projects/37" }.should route_to(:controller => 'projects', :action => 'update', :id => '37')

    { :get => "/nonexisting_route" }.should_not be_routable

route_toの方が分かりやすい。
rdocにも下記のように記載があります。

Note that this method is obsoleted by the route_to matcher.

RSpec.info: Controllersのリンク直して欲しいな。

RSpecのスライドがいい

Kerry Buckleyさん?のRSpecのスライドが凄くいいです。
RSpec


3分の1くらい知らない内容が載っていた感じです。
読みやすくとても良く纏まってるので
また何度か読み返すと思います。

気になったとこ

.rspec

spec.opts ってdeprecateなんですね。
.rspecファイルらしいです。

fuubar

テストの進行具合をプログレスバー表示してくれる
かわいいツールです。早速installしてみます。

$ gem install fuubar
Fetching: rspec-instafail-0.1.5.gem (100%)
Fetching: fuubar-0.0.3.gem (100%)
Fetching: rspec-core-2.4.0.gem (100%)
**************************************************

  Thank you for installing rspec-core-2.4.0

  Please be sure to look at the upgrade instructions to see what might have
  changed since the last release:

  http://github.com/rspec/rspec-core/blob/master/Upgrade.markdown

**************************************************
Fetching: rspec-expectations-2.4.0.gem (100%)
Fetching: rspec-mocks-2.4.0.gem (100%)
Successfully installed rspec-instafail-0.1.5
Successfully installed fuubar-0.0.3
Successfully installed rspec-core-2.4.0
Successfully installed rspec-expectations-2.4.0
Successfully installed rspec-mocks-2.4.0

実行結果

$ rspec -f Fuubar spec/
  56/56:       100% |==========================================| Time: 00:00:00

Finished in 0.55649 seconds
56 examples, 0 failures

まぁ、悪くないです。


profile
$ rspec --profile spec -c
........................................................

Top 10 slowest examples:
  Majang 1112345678999 
    0.07475 seconds ./spec/lib/majang_spec.rb:39
  Majang 1112345678999 
    0.07276 seconds ./spec/lib/majang_spec.rb:47
  App GET / 
    0.06609 seconds ./spec/app_spec.rb:7
  Majang 1112345678999 
    0.02858 seconds ./spec/lib/majang_spec.rb:46
  Majang 1112345678999 
    0.0189 seconds ./spec/lib/majang_spec.rb:45
  Majang 1112345678999 
    0.01833 seconds ./spec/lib/majang_spec.rb:38
  Majang 1112345678999 
    0.01811 seconds ./spec/lib/majang_spec.rb:42
  Majang 1112345678999 
    0.01583 seconds ./spec/lib/majang_spec.rb:37
  Majang 1112345678999 
    0.01583 seconds ./spec/lib/majang_spec.rb:43
  Majang 1112345678999 
    0.01547 seconds ./spec/lib/majang_spec.rb:48

Finished in 0.52669 seconds
56 examples, 0 failures

exampleの処理時間でソートしてくれます。
これも悪くないです。

filtering

tag、version、名前によるfiltering機能があるんですね。
知りませんでした。

let

懐かしいbowling gameが例に出されていました。

describe BowlingGame do
  let(:game) { BowlingGame.new }
 
  it "scores all gutters with 0" do
    20.times { game.roll(0) }
    game.score.should == 0
  end
 
  it "scores all 1s with 20" do
    20.times { game.roll(1) }
    game.score.should == 20
  end
end

letを指定することで、インスタンス変数の使用が抑えられます。見た感じすっきりします。
(多分、ここらへん一度勉強したけど忘れてたっぽいなぁ)

routing

railsのroutingテストもできるとは。



などなど、rails周りのmockやstubも一通り記載されていて
凄く充実してます。

麻雀スクリプトのテストコード


rubyで麻雀の待ちを出力 - うんたらかんたら日記
privateメソッドのテスト - うんたらかんたら日記

テストコード

書いてみた。

#spec/majang_spec.rb
require File.expand_path(File.dirname(__FILE__) + '/../majang.rb')

describe Majang do
  before do
    @m = Majang.new
  end

  describe "待ち無し1112224788899" do
    before { @m.mati("1112224788899") }
    subject { @m.tenpai}
    it { should have(:no).item }
  end
  describe "1112224588899" do
    before { @m.mati("1112224588899") }
    subject { @m.tenpai}
    it { should have(1).item }
    it { should == ["(111)(222)(888)(99)[45]"] }
  end
  describe "1112223335559" do
    before { @m.mati("1112223335559") }
    subject { @m.tenpai}
    it { should have(2).items }
    it { should include("(111)(222)(333)(555)[9]") }
    it { should include("(123)(123)(123)(555)[9]") }
  end
  describe "1223344888999" do
    before { @m.mati("1223344888999") }
    subject { @m.tenpai}
    it { should have(3).items }
    it { should include("(123)(234)(888)(999)[4]") }
    it { should include("(123)(44)(888)(999)[23]") }
    it { should include("(234)(234)(888)(999)[1]") }
  end
  describe "1112345678999" do
    before { @m.mati("1112345678999") }
    subject { @m.tenpai}
    it { should have(11).items }
    it { should include("(111)(234)(567)(99)[89]") }
    it { should include("(111)(234)(678)(999)[5]") }
    it { should include("(111)(234)(789)(99)[56]") }
    it { should include("(111)(234)(567)(999)[8]") }
    it { should include("(111)(345)(678)(999)[2]") }
    it { should include("(111)(456)(789)(99)[23]") }
    it { should include("(11)(123)(456)(789)[99]") }
    it { should include("(123)(456)(789)(99)[11]") }
    it { should include("(123)(456)(789)(99)[11]") }
    it { should include("(11)(123)(678)(999)[45]") }
    it { should include("(11)(345)(678)(999)[12]") }
  end


  # private methods
  describe ":format_tenpai" do
    context '["123", "456"],["78"]' do
      it { @m.send(:format_tenapi, ["123","456"], ["78"]).should == "(123)(456)[78]" }
    end
  end

  describe ":all_same?" do
    context "要素が0個の場合" do
      it { be_all_same?([]) }
    end
    context "素が1個の場合" do
      it { be_all_same?(["1"]) }
    end
    context "全ての要素が同じ場合" do
      it { be_all_same?(["1", "1", "1"]) }
    end
    context "異なる要素が存在する場合" do
      it { not be_all_same?(["1", "2", "1"]) }
    end
  end

  describe ":tenpai?" do
    before do
      #tenpai?の中で呼んでいるappend_tenpaiで<<がエラーとなるため、stubを作成
      @m.stub!(:append_tenpai)
    end
    #単騎待ちは考慮しない
    context '両面待ちの場合["1", "1", "2", "3"]' do
      it { be_tenpai?(["1", "1", "2", "3"]) }
    end
    context 'カンチャン待ちの場合["1", "1", "4", "6"]' do
      it { be_tenpai?(["1", "1", "4", "6"]) }
    end
    context 'ペンチャン待ちの場合["1", "1", "8", "9"]' do
      it { be_tenpai?(["1", "1", "8", "9"]) }
    end
    context 'シャンポン待ちの場合["1", "1", "2", "2"]' do
      it { be_tenpai?(["1", "1", "2", "2"]) }
    end
    context '待ちなし場合["1", "1", "2", "5"]' do
      it { not be_tenpai?(["1", "1", "2", "5"]) }
    end
  end

  describe ":mati?" do
    context '要素が差が0の場合["1", "1"]' do
      it { be_mati?(["1", "1"]) }
    end
    context '要素が差が1の場合["1", "2"]' do
      it { be_mati?(["1", "2"]) }
    end
    context '要素が差が2の場合["1", "3"]' do
      it { be_mati?(["1", "3"]) }
    end
    context '要素が差が3の場合["1", "4"]' do
      it { not be_mati?(["1", "4"]) }
    end
  end

  describe ":candidates" do
    context '順子が存在しない場合["1","1","2"]' do
      it { do_candidates(["1","1","2"]).should == [] }
    end
    context '刻子が存在する場合["1","1","1","2"]' do
      it { do_candidates(["1","1","1","2"]).should == ["111"] }
    end
    context '刻子と順子が存在する場合["1","1","1","2","2","2","3","3","3"]' do
      it { do_candidates(["1","1","1","2","2","2","3","3","3"]).should == ["111","222","333","123"].sort }
    end
  end

  private
  def all_same?(arg)
    @m.send(:all_same?, arg)
  end
  def tenpai?(arg)
    @m.send(:tenpai?, arg, [])
  end
  def mati?(arg)
    @m.send(:mati?, arg)
  end
  def do_candidates(arg)
    @m.send(:candidates, arg)
  end
end

describe Array do
  context "重複要素を引いた場合" do
    it "1つのみ削除されること" do
      ["1", "1", "2"].minus(["1"]).should == ["1","2"]
    end
  end
  context "複数要素を引いた場合" do
    it "正しく引き算されること" do
      ["1", "1", "2","3"].minus(["1","2"]).should == ["1","3"]
    end
  end
  context "引き算対象のデータが無い場合" do
    it "selfが返却されること" do
      ["1", "2", "3"].minus(["4"]).should == ["1","2","3"]
    end
  end
end

もう少し何かすればきれいになるような気がするが。

gist: 764900 - GitHub

privateメソッドのテスト


麻雀スクリプトを書く際にrspec書きながらやろうと思ったんだけど
試行錯誤重ねる段階でテストがうまく書けなかった。
いつも考えて試しながら書くとテストがちゃんと書けない。
rubyで麻雀の待ちを出力 - うんたらかんたら日記


これでは進歩が無いので、privateメソッドのテストを書くところから、
まずはやってみた(というかまだ途中だけど)。

書き方

#spec/majang_spec.rb
require File.expand_path(File.dirname(__FILE__) + '/../majang.rb')

describe Majang do
  # private methods
  describe ":format_tenpai" do
    context '["123", "456"],["78"]' do
      it { @m.send(:format_tenapi, ["123","456"], ["78"]).should == "(123)(456)[78]" }
    end
  end
end

結果

Majang
  :format_tenpai
    ["123", "456"],["78"]
      should == "(123)(456)[78]"

参考

UKSTUDIO - RSpecでprivateメソッドをテストする
sendを使えばいいんですね。