web-k.log

RubyやWebをメインに技術情報をのせていきます

RSpecとCapybaraのマッチャ比較

| Comments

Ruby on RailsでSpecを書く時に、RSpecのマッチャなのかCapybaraのマッチャなのか分からなくなったのでメモ

RSpecのマッチャ

RSpecのマッチャには、「演算子マッチャ」と「ビルトインマッチャ」があり、「should」や「should_not」と組み合わせて使用します。 (shouldとshould_notはRSpecがObjectを拡張して作ったメソッド)

マッチャとして使用できる演算子は、「<」、「<=」、「==」、「===」、「=~」、「>」、「>=」の7種類。

使うときは以下のようにして記載します。

1
2
3
(1 + 2).should == 3
1.should < 2
"apple".should_not =~ /orange/

なお、「!=」や「!~」などの否定演算子はサポートされていません。「なんとかではないこと」を記載するときはshouldの代わりにshould_notを使います。

ビルトインマッチャは

  • be
  • be_a
  • be_a_kind_of
  • be_an_instance_of
  • be_close
  • be_within
  • change
  • eq
  • eql
  • equal
  • exist
  • expect
  • have
  • have_at_least
  • have_at_most
  • include
  • match
  • raise_error
  • respond_to
  • satisfy
  • throw_symbol

って感じでたくさんあります。 「be_XXX」マッチャは「be_a_XXX」、「be_an_XXX」と記載しても同じ動作になります。 「have_XXX」マッチャは、RSpec の実行時には 「has_XXX?」メソッドの呼び出しとして解釈されます。これは、should と並べ たときの字面を (英語として) 自然な記述するための措置です。

使うときは以下のようにして記載します。

1
2
3
"string".should be_an_instance_of(String)
1.should be_a_kind_of(Numeric)
1.should_not be_an_instance_of(String)

また、マッチャを独自定義して使用することも可能です。 次のように書きます。(公式ドキュメントから引用)

1
2
3
4
5
Spec::Matchers.define :be_in_zone do |zone|
  match do |player|
    player.in_zone?(zone)
  end
end

Railsで使う場合は、spec/フォルダ直下にcustom_matchers.rbを作り、

custom_matchers.rb

1
2
3
4
5
module CustomMatcher
  def custom_matcher
    #マッチャの処理を記述 
  end
end

spec/spec_helper.rbを編集

1
2
3
4
require File.dirname(__FILE__) + "/custom_matchers"

RSpec.configure do |config|
  config.include(CustomMatcher)

これでマッチャとしてcustom_matcherが使えるようになります。

Capybaraのマッチャ

Capybaraのマッチャは、オブジェクト対してメソッドの様に記載したり、RSpecのマッチャの様に記載することができます。 形式は「has_xxx?」の形をしているものがほとんど。

  • assert_selector
  • has_button? / has_no_button?
  • has_checked_field? / has_no_checked_field?
  • has_content? / has_no_content?
  • has_css? / has_no_css?
  • has_field? / has_no_field?
  • has_link? / has_no_link?
  • has_select? / has_no_select?
  • has_selector? / has_no_selector?
  • has_table? / has_no_table?
  • has_text? / has_no_text?
  • has_unchecked_field? / has_no_unchecked_field?
  • has_xpath? / has_no_xpath?

って感じでこちらもたくさんあります。

使うときは以下のようにして記載します。

1
2
3
4
5
6
page.has_xpath?('//table/tr')
page.has_css?('table tr.hoge')
page.has_no_content?('hoge')
# page.has_XXX? は true/false を返すだけなので、テスト結果を得たいときは、
# assert page.has_xpath?('//table/tr')
# というようにassertを付けて記載します。

また、RSpecのマッチャの様に記載するときは「has_XXX?」を「have_XXX」に直して以下のようにします。

1
2
3
page.should have_xpath('//table/tr')
page.should have_css('table tr.hoge')
page.should have_no_content('hoge')

このとき、page.should have_no_XXX(‘value’)をpage.should_not have_XXX(‘value’)とは書かないほうがよいです。 理由はAjaxの待ち時間を考慮するためです。 Capybaraには非同期のJavascriptを扱う時に、まだページに存在していないされていないDOM要素を一定の待ち時間が経過したら再度探すという振る舞いがあります。 shouldを用いたときにその振る舞いは発揮されますが、should_notを用いたときはその振る舞いは発揮されず1回目の捜査で終了します。 待ち時間は次のように変更することができます。(デフォルトは2秒です。)

1
Capybara.default_wait_time = 3

参考リンク

RSpecまとめ(2)~Mock(double/stub/mock)~

| Comments

前回はRSpecの基本メソッドについてまとめました。今回はMockについてまとめます。

テストダブルとは

テスト対象が依存しているモジュールやリソースの代役のこと。結合テストのような複雑な環境を事前に用意せずとも目的の機能をテスト可能となるように振る舞いをシミュレートする。

irb,pry等でMockを試したい時、

1
require 'rspec/mocks/standalone'

をrequireすると試せるのでやってみると良い

double/stub/mock/stub_chain

おすすめの使い方で説明。他にも色々出来るとは思う

  • double - ダミーオブジェクト作成
1
2
3
4
# ダミーオブジェクト作る。第1引数省略可能だが、なんかエラーとか出たら表示されるので入れておくと良い
book = double("book")
# book.title呼んだら"The RSpec Book"と返ってくるダミーオブジェクト
book = double("book", :title => "The RSpec Book")
  • stub - ダミーメソッド作成
1
2
3
4
5
6
# 全部同じ。book.title呼んだら"The RSpec Book"と返ってくるダミーオブジェクト
book.stub(:title) { "The RSpec Book" }
book.stub(:title => "The RSpec Book")
book.stub(:title).and_return("The RSpec Book")
# doubleで作ったダミーオブジェクトでなくてもダミーメソッドを定義できる
String.stub(:test).and_return('test')
  • mock - メソッドの振る舞いを評価するためのダミーオブジェクト作成
1
2
3
4
5
6
7
8
9
10
11
12
13
# logger.log('... Statement generated for Aslak ...') が呼ばれることを確認したい
it "logs a message on generate()" do
  # テストの為にcustomer.nameで'Aslak'を返しておきたいのでdouble/stub
  customer = double('customer')
  customer.stub(:name).and_return('Aslak')
  # 今回の評価対象なのでmock
  logger = mock('logger')
  statement = Statement.new(customer, logger)
  # logger.log('... Statement generated for Aslak ...') が呼ばれることを期待
  logger.should_receive(:log).with(/Statement generated for Aslak/)
  # テスト対象を実行して期待通りかテスト 
  statement.generate
end
  • stub_chain - メソッドチェインの簡単な作成
1
Article.recent.published.authored_by(params[:author_id])

上記のメソッドチェインをまとめてstub化できる:

1
2
article = double()
Article.stub_chain(:recent, :published, :authored_by).and_return(article)

stubとmockの違い

doubleもstubもmockもSpec::Mocks::Mockのインスタンスを作成します。テスト用途によって利用方法を振り分けます。

  • stub - 呼び出しに対して決められた値を返します。結合すると環境構築が面倒だったり外部のサーバ状況に依っては失敗してしまうようなモジュール境界、ランダム値が返るオブジェクト等でダミーで返したい時に利用
  • mock - どのように呼び出しされるか、そのダミーオブジェクトの振る舞い自体を評価したい場合に使用

rspec-railsでのMock拡張

  • mock_model - ダミーのmodelオブジェクトを作成します。ActiveRecordオブジェクトではなく、最低限のメソッドのみ対応しています。Controllerテストの様に正しくモデルを呼び出されているかチェックする時に利用します
1
2
3
4
5
6
7
8
# 評価対象はUser
user = mock_model(User)
# User.find_by_idが呼ばれた時にmockを返しておく
User.stub(find_by_id: user)
# user.saveが呼ばれることを期待
user.should_receive(:save).and_return(true)
# 期待通りに振る舞うかリクエスト
put '/users/1', login: 'zdenis'
1
2
# 保存前のダミーモデルオブジェクトとして振る舞う
new_user = mock_model(User).as_new_record
  • stub_model - ActiveRecordオブジェクト本物をダミーとして作成します。Viewテストの様にModelを操作することが重要ではなく、値を参照して処理した結果、意図通りの生成結果となったかチェックする時に利用します
1
2
3
4
# @userに@user.loginが'zdennis'、@user.emailが'zdennis@example.com'のUserオブジェクトを登録
assign(:user, stub_model(User, login: 'zdennis', email: 'zdennis@example.com'))
render
renderd.should have_selector(...)

参考リンク

RSpecまとめ(1)~基本メソッド~

| Comments

RSpecで使う基本メソッド(describe/context/it/its/before/after/subject/let/shared_examples_for)をまとめてみる。

参考リンク

基本メソッド

  • describe/context - テスト名。テスト対象自身のオブジェクト(subject代わり)でもOK
  • before - 事前条件。example(it)の前に実行される。before :each はexample毎、before :all はdescribe毎に呼ばれる
  • after - 事後処理。以後のテストに影響が出ないように後始末が必要な時に記述する
  • subject - 評価対象。shouldの前のオブジェクトを指定することでit内のオブジェクトを省略できる
  • it - テスト仕様。マッチャーを使って評価する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 一番上のdescribeはテスト対象にしておくといい。subjectにもなる
describe Controller do
  # contextはdescribeのalias。テスト対象にgetのときとか事前条件変えるときはこっちの方が読みやすいかも
  context "handling AccessDenied exceptions" do
    # デフォルトはbefore :each
    # 事前条件はitにも書けるけど、なるべく分けるように
    before { get :index } # 1行になるべく書く

    # itのテスト対象。shouldの前を省ける
    subject { response }

    # itの後に概要書けるけど、マッチャーだけで意味通るなら省略する
    it { should redirect_to("/401.html") }
  end
end
  • it(context, tags) - タグが付けられる
$ rspec --tag ruby:1.8
1
2
3
4
5
6
7
8
9
10
11
describe "Something" do
  # ruby:1.8
  it "behaves one way in Ruby 1.8", :ruby => "1.8" do
    ...
  end

  # ruby:1.9
  it "behaves another way in Ruby 1.9", :ruby => "1.9" do
    ...
  end
end
  • its(method) - subjectのメソッドが呼べる
1
2
3
4
5
6
7
describe [1, 2, 3, 3] do
  # [1,2,3,3].size.should == 4
  its(:size) { should == 4 }

  # [1,2,3,3].uniq.size.should == 3
  its("uniq.size") { should == 3 }
end
  • let - example内で同じオブジェクトの使い回し
1
2
3
4
5
6
7
8
9
10
11
12
13
14
describe BowlingGame do
  # example毎に呼ばれる。でも遅延評価。使わなかったら呼ばれない
  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
  • shared_examples_for - exampleの共通化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shared_examples_for "a single-element array" do
  # letやbeforeやafterも書ける。要はなんでも共通化
  let(:xxx) { Obj.new }
  before { puts 'before' }
  after { puts 'after' }

  # subjectやletを渡せる
  it { should_not be_empty }
  it { should have(1).element }
end

describe ["foo"] do
  it_behaves_like "a single-element array"
end

describe [42] do
  it_behaves_like "a single-element array"
end
  • shared_context - 事前条件の共通化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
shared_context '要素がふたつpushされている' do
  let(:latest_value) { 'value2' }
  before do
    @stack = Stack.new
    @stack.push 'value1'
    @stack.push latest_value
  end
end

describe Stack do
  describe '#size' do
    include_context '要素がふたつpushされている'
    subject { @stack.size }
    it { should eq 2 }
  end

  describe '#pop' do
    include_context '要素がふたつpushされている'
    subject { @stack.pop }
    it { should eq latest_value }
  end
end

次回はMockについてまとめてみます。