@itachi-PのRailsを中心とした学習記録&ポートフォリオ作成メモ 学習効率上アウトプットを増やす必要性を感じた為、Qiitaと使い分け毎日気軽に自分用メモをアウトプットする場、計画性を身に付けるセルフコントロールの一環として2019/3/22開始 強烈な自己否定・批判癖が染み付いてるので客観的に問題点を見据えながらバランス取りつつ<楽しく>記述 基本的に文章書くこと自体は好きっぽい

今日やったこと

RSpec基本

実行環境
Ruby 2.5.3 (global)
RSpec 3.8

マッチャ(matcher)いろいろ
  • to / not_to / to_not
  • eq
  • be
  • be_xxx
  • be_truthy / be_falsey
  • change + from / to / by
  • 配列 + include
  • raise_error
  • be_within + of

マッチャ(matcher)とは

expect(1 + 2).to eq 3  
expect([1, 2, 3]).to include 2  

上記のサンプルでいえばeqinclude
toは厳密にはマッチャではない)
実際の値(1 + 2[1, 2, 3])と期待値(32)を比較し、ルールに合致しているかどうかを判断する一種の演算子

to / not_to / to_not

  • to 「~であること」を期待する場合
  • not_to または to_not 「~ではないこと」を期待する場合

tonot_to自体はマッチャではない
マッチャの実行結果を受け取り、テストをパスさせるか否かを判断するRSpecのメソッド

not_toto_notは機能的には全く同じ
読みやすい方を使えばよい
it, specify, exampleと同じく「英文としてより自然にする為」に用意されたaliasの関係
ただ、RSpecの公式ドキュメントのサンプルコードは not_to をよく使っている

eq

  • 期待値と実際の値が「等しい」かどうかを検証する
  • 最もよく使われる
    expect(1 + 2).to eq 3  

be

  • 等号・不等号と組み合わせて、値の大小を検証するときによく使われる
    expect(1 + 2).to be >= 3  

(予備知識) beとeqの違い

  • beとeqは似たような用途で使うこともできる
  • ただし、eqは同じ値ならばtrueを返すが、beは値が同じでも別のインスタンスならfalseを返す点に注意
  • この点を理解していないと予想外の挙動に悩むことになる
    (実例)
    beは等号・不等号なしで次のように書くこともできる
    message = 'Hello'  
    expect([message].first).to be message  
    等号・不等号なしで書いた場合は、2つの値が同一のインスタンスかどうかを検証する
    つまり、equal? メソッドの結果が true かどうかを検証している

したがって以下のようなケースではインスタンスが異なる為にテストは失敗する

message_1 = 'Hello'  
message_2 = 'Hello'  
# message_1 と message_2 は異なるインスタンスなのでテストが失敗する  
expect([message_1].first).to be message_2  

eqの場合は==で比較しているので、値が同じならばtrueを返す

message_1 = 'Hello'  
message_2 = 'Hello'  
# message_1 == message_2 の結果は真になるのでテストはパスする  
expect([message_1].first).to eq message_2  

通常は同一インスタンスであるかどうかを検証する機会はあまり多くないと思われるので、大半のテストケースではbeではなくeqを使っておけばOK

ただし、true / false / nil や、整数値、シンボルは特殊で、「同じ値であれば同じインスタンス」になるため、be を使ってもテストはパスする(eq でもパスする)。

# true / false / nil はいつでも同じインスタンス  
expect(true).to be true  
expect(false).to be false  
expect(nil).to be nil  

# 整数値やシンボルは、同じ値はいつでも同じインスタンス  
expect(1 + 1).to be 2  
expect(:foo).to be :foo  

シンボルの場合、タイプミスが無い限り同じ文字列ならば同じインスタンス(同一オブジェクトID)になるが、通常の変数・全く同じ文字列でも内部的には違うインスタンスになる、というのは以前学んだ通り
(Rails console・IRB上等でもシンボルや変数に対し個々のオブジェクトIDを調べることが可能)

が、以上の仕様を理解した上でも不等号を用いた数の大小比較以外のケースでわざわざeqではなくbeを使う理由は特に無いものと思われる

・・・ここまでの学習で気付いたこと

いかん、時間かけ過ぎだ
もっと駆け足で読み流すだけでいい内容を逐一記録することはないし
別に「ちゃんとこれだけ学習しました」と証明を残す為に記録するわけでもない。
というわけで以降は読み流すだけで充分と思われる部分の記録はすっ飛ばす。

be_truthy / be_falsey

class User < ActiveRecord::Base  
  validates :name, :email, presence: true  
end  
# 必須項目が入力されていないので保存できない(結果はfalse)  
user = User.new  
expect(user.save).to be_falsey   

# 必須項目が入力されているので保存できる(結果はtrue)  
user.name = 'Tom'  
user.email = 'tom@example.com'  
expect(user.save).to be_truthy  
  • Rubyでは「false または nil であれば偽、それ以外は全て真」と評価される
  • 一方、 be true / be false (または eq true / eq false) を使うと true もしくは false であることを厳密に検証するので、それ以外の値を渡されるとテストが失敗する
# どちらもパスする  
expect(1).to be_truthy  
expect(nil).to be_falsey  

# どちらも失敗する  
expect(1).to be true  
expect(nil).to be false  

# be の代わりに eq を使った場合も同様に失敗する  
expect(1).to eq true  
expect(nil).to eq false  

以上より、RSpecで真偽値を確認するテストを書く場合は、何か特別な事情がない限り be_truthy / be_falsey を使った方が「Rubyらしい真偽値のテスト」が可能

change + from / to / by

# popメソッドを呼ぶと配列の要素が減少することをテストする  
x = [1, 2, 3]  
expect(x.size).to eq 3  
x.pop  
expect(x.size).to eq 2  

上のテストは change マッチャを使うと次のように書き換えることができる。

x = [1, 2, 3]  
expect{ x.pop }.to change{ x.size }.from(3).to(2)  

expect{ X }.to change{ Y }.from(A).to(B) = 「X すると Y が A から B に変わることを期待する」

change のバリエーションとして by を使った書き方
by を使うと「(元の個数はともかく)1個減ること」を検証できる

x = [1, 2, 3]  
expect{ x.pop }.to change{ x.size }.by(-1)  

もちろん増える場合も検証可能

x = [1, 2, 3]  
expect{ x.push(10) }.to change{ x.size }.by(1)  
changeを使った応用例

例として、Railsで「userを削除すると、userが書いたblogも削除されること」を検証

class User < ActiveRecord::Base  
  # dependent: :destroy を付けたので、userを削除するとblogも削除される  
  has_many :blogs, dependent: :destroy  
end  

class Blog < ActiveRecord::Base  
  belongs_to :user  
end  
it 'userを削除すると、userが書いたblogも削除されること' do  
  user = User.create(name: 'Tom', email: 'tom@example.com')  
  # user が blog を書いたことにする  
  user.blogs.create(title: 'RSpec必勝法', content: 'あとで書く')  

  expect{ user.destroy }.to change{ Blog.count }.by(-1)   
end  

このように change マッチャを使うと、「Xに対するある操作が、一見無関係なYに影響を与える」といった検証内容を簡潔に表現することができる。

(個人的所感)by(-1)よりもeq 0の方が汎用的かつ完全な気がするが…
※文法的にchangeの後に続けてeq 0 と書けるのかどうかは未検証


その他そこそこの使用頻度のマッチャ

配列 + include

include マッチャを使うと、「配列に~が含まれていること」を検証することができます。

x = [1, 2, 3]  
# 1が含まれていることを検証する  
expect(x).to include 1  
# 1と3が含まれていることを検証する  
expect(x).to include 1, 3  

他にも include はハッシュや文字列に対しても使うことができる

raise_error

「エラーが起きること」を検証する場合は、 raise_error マッチャを使う

「0で除算するとエラーが起きること」は次のようにテストできる

expect{ 1 / 0 }.to raise_error ZeroDivisionError  

change マッチャと同様、expect にはブロック(中括弧)を渡している点に注意

もう少し実践的なサンプルコード
自作クラスで明示的にエラーを返すようにしたメソッドをテスト

class ShoppingCart  
  def initialize  
    @items = []  
  end  
  def add(item)  
    raise 'Item is nil.' if item.nil?  
    @items << item  
  end  
end  
it 'nilを追加するとエラーが発生すること' do  
  cart = ShoppingCart.new  
  expect{ cart.add nil }.to raise_error 'Item is nil.'  
end  

上の例では raise_error の引数としてエラーメッセージ(文字列)を渡している
このように raise_error にはエラーのクラスやエラーメッセージ(文字列)など、いろいろな引数を渡すことができる
詳しい使い方はRSpecの公式ドキュメント参照

be_within + of

be_within(Y).of(X) で「数値 X がプラスマイナス Y の範囲内に収まっていること」を検証する

おまけ

class You  
  def read_this_entry  
    @matcher_expert = true  
  end  
  def matcher_expert?  
    @matcher_expert  
  end  
end  
describe You do  
  describe '#read_this_entry' do  
    let(:you) { You.new }  
    it 'この記事を読むとマッチャを使いこなせるようになっていること' do  
      expect{ you.read_this_entry }.to \  
        change{ you.matcher_expert? }.from(be_falsey).to(be_truthy)  
    end  
  end  
end  

次は明日以降でRSpecの入門とその一歩先へ ~RSpec 3バージョン~

今日の夕食後残りの時間はガッツリ読書か久々にProgateでJavaScript(React || jQuery)にしよう。

今日やったこと(追加)

  • 久々のProgate jQuery(初級)
    • ProgateのjQueryを最後までやるかは状況次第
    • 後に回したReactの方を優先し、jQueryは軽く流す程度で履修
    • ローカル環境で実際に手を動かしてJavaScriptを動かし、Railsアプリに組み込む準備を進める

このログへのコメント

コメントはありません