Railsチュートリアル卒業者に捧げる、RSpec超入門

rubyアイキャッチRSpec

どうも、ウェブ系フリーランスのウェブ系ウシジマくんです!

今回の記事では、Rubyのテストフレームワークで有名なRSpec(アールスペック)について取り上げます。

このRSpecは書き方が特殊なので使いこなすには学習コストがかかりますが、一度覚えればかなり直感的にテストがかけるようになります。

ほとんどの企業がこのRSpecを採用しているので、実際に現場で働く場合に出てくる単語を知っておくだけでもキャッチアップしやすくなるでしょう。

今回の記事の対象者

  • RSpecという名前は聞いたことあるけど、いまいち良くわからない人
  • 次に参画する現場で、RSpecを使うことになった人
  • WebでRSpecについて調べても、書き方がよくわからない人

この記事を理解するのに必要なレベル感

  • Railsチュートリアルを一通りやり終えた人
  • Qiitaなどで触りだけでもRSpecについて学んだことがある人

今回の記事を通じてできるようになること

  • RSpec内で出てくる単語の意味がわかるようになる
  • RSpecで使用するメソッドの使われ方がわかるようになる

beforeブロックについて

beforeブロックは、specを実行する前に事前に行っておきたい処理を記述するゾーンです。

例えば、ユーザーのインスタンスを作ったり、性別やメールアドレスの属性値を持たせたい場合などですね。

before(:example) do
  @calc = Calc.new
end

ここでは、Calcというクラスオブジェクトからインスタンスを生成し、それを@calcというインスタンス変数に代入しています。

ちなみに、before(:example) do end; とbefore(:each) do end; は互いにエイリアスなので全く同じ意味になります。

Qiitaやブログ記事でbefore(:example)やbefore(:each)が出てきたら、どちらも同じことをしているのだと思ってください。

ちなみに、

before do
  @calc = Calc.new
end

このように引数を省略して書いても同じ挙動になるので、ほとんどの場合はこちらのように書くことが多いですね!

ポイント:before(:each)ブロックの中に書いた処理が、テスト実行前に毎回呼ばれる。

before(:context)はちょっと挙動が違う

beforeの引数にcontextやallを渡すと、ブロック内で記述した処理をテスト実行前に1回だけ呼びだそうとします。

before(:context) do
  @calc = Calc.new
end

毎回呼び出すわけではないので、ここを理解しておかないと、後述するマッチャーを呼び出した時にテストが失敗するので要注意。

先ほども説明したように、beforeの引数を省略すると自動的にeach(example)が呼び出されるので、テスト前に1度だけ実行する処理を行わせたい場合は、before(:context)を使いましょう。

なお、before(:context)とbefore(:all)も互いにエイリアスなので全く同じ意味です。

ポイント: before(:context)ブロック内で書いた処理が最初の1回だけ呼び出す。

実行結果を色分けで表示する

rspe -fd

RSpecではデフォルトだと、

成功の場合 => .
失敗の場合 => F

このような表示結果になります。

ところが、rspec コマンドに「-fd」というオプションをつけることで、文章が色分けされて表示されるようになります。

どんなテストが実行されて、どんな実行結果になったのかがわかりやすいので、自分以外の人が書いたテストを実行させる時に使うと便利かも。

ちなみに、-fdは (f)ormat (d)ocumentation の略ですね。

contextとdescribeの使い分け

describecontextと書いても同じ意味になります。

ただ慣習としては、

  • テスト対象がモノなど =>「describe」
  • テスト対象が状況 =>「context」

のように使われることが多いようです。

「describe」でも「context」でも、そのあとに渡す文字列は好きなものを使えます。

なお、トップレベル(RSpec.describeの部分)の「describe」はテスト対象となるクラス名を書いたりすることがあるので、合わせて覚えておくといいかもしれません。

it/example/specifyの使い分け

どれも互いにエイリアスなので、全く同じ挙動になります。

それぞれの引数に渡す文字列は日本語も渡せるので、日本語の場合はぶっちゃけどれを使っても問題ないでしょう。

強いて言えば、そのプロジェクト内で最もよく使われているものを使えばいいと思いますよ。

ただ、英語で文字列を書く場合は文章として自然になるように、

  • It… これ/それ
  • example… 例/実例
  • Specify…~をはっきり述べる

こんな感じにそれぞれ英語の意味を踏まえながら使い分けた方がいいですね。

ちなみに、itなどの後にブロックを渡さないと、pendingとなり、rpsecコマンドでテストを実行した際に対象のexampleがスキップされます。

matcher(マッチャーについて)

マッチャーとターゲット

マッチャーは、ターゲットに対して期待される振る舞いの指定をするための記号のようなものです。

ターゲットというのは、expectの後に記述する括弧の中にある変数や文字列を指しています。

ターゲットの直後にはtoをつけ、マッチャーを使って期待通りに(expect)処理が実行されるかテストすることになります。

なお、テストでターゲットの振る舞いを否定させたい時には、ターゲットの直後にnot_toまたはto_notを使うことになります。

expect(calc.add(2, 3)).not_to eq(5)

not_toとto_not、どちらを使っても挙動に変わりはないですが、ほとんどの場合はnot_toを使う事が多いですね。

こちらもto同様に全てのマッチャーに対して使う事ができます。

さて、次からはそれぞれのマッチャーについて代表的なものを取り上げて行きます。

eq

# 使用例
expect(calc.add(2, 3)).to eq(5) 

expectの引数に渡したターゲットが、eqの引数に渡したものと等しいことを表します。

true/false

# 使用例
expect(calc.add(2, 3)).to be true
expect(calc.add(2, 3)).to be false

expectの引数に渡したターゲットが、真または偽であることを表します。

>/</<=/>=

# 使用例
expect(calc.add(2, 3)).to be > 10

範囲を指定したい場合、

10より小さい => 「be < 10」、
10より大きい => 「be > 10」

のように不等号記号を使えばOKです。

be_between(int1, int2).inclusive

# 使用例
expect(calc.add(2, 3)).to be_between(1, 10).inclusive

数値の範囲を指定します。int1からint2の間で、int1とint2を含むことを表しています。

respond_to(:hoge)

# 使用例
expect(calc).to respond_to(:hoge)

オブジェクトに対してメソッドの存在を調べるマッチャーです。引数に渡すメソッドはシンボルで渡せばOK。

be_hoge(メソッド名)

# 使用例
expect(calc.add(2, 3).hoge?).to be true 
expect(calc.add(2, 3)).to be_hoge # 上と同じ意味になる

expect(calc.add(2, 3)).to be_hoge」とすると、「expect(calc.add(2, 3).hoge?).to be_true」とまったく同じ意味になります。

RSpecではメソッド名のprefix(先頭)にbe_をつけることで、末尾に?をつけた真偽値を返すメソッドをターゲットのオブジェクト(expectの引数)に対して呼び出すことができ流ようになります。

戻り値がtrueならテストは成功し、falseまたはnilであれば失敗と判定されます。

subjectの使い方

Subjectを使うことで、トップレベル(RSpec.describeの箇所)のdescribeで「Klass」のようにクラス名を指定した場合、そのクラスのインスタンスを代用することが可能です。

subejectを使わないと、klass = Klass.newとしなければインスタンス化できませんが、

expect(subject.add(2, 3)).to eq(5)

subjectを使えば、上記のように書くことができるのです。

ちなみに、以下のように書けばテストが長くなってもsubjectが何を指しているかが明確になります。

subject(:klass) { Klass.new }

これなら、exampleの中でインスタンス変数を使わなくて済むので、第三者が見たときに混乱しないですよね。

beforeを使って、

before { @calc = Calc.new end }」

上記のように書いてもいいですが、これだとインスタンスを呼び出す際にいちいち@をつけないといけないので面倒です。

なるべくexampleの中ではインスタンス変数を使わない方がよいとされているらしく、

「subject(:calc) { Calc.new }」

このような書き方の方がいいらしいです。

letについて

letは操作のメインターゲット以外で変数のように何かを管理したい場合に使うことができます。

書き方は「let()」として、subjectと同じようにシンボルを渡し、そのあとにブロックで値を渡せばOK。

ただし、letは次のような特徴があります。

  1. exampleごとに結果がキャッシュされる
  2. 遅延評価

exampleごとに結果がキャッシュされるというのは、letでインスタンス化したメソッドは最初に評価された値が使い回されるので、実行される度に値が変わることはないという事。

遅延評価されるというのは、letでインスタンス化したメソッドを呼び出されないと、ブロック内の値が評価されないということです。

例えば、次のようなitブロックがあった場合、

it { expect(calc.price(100)).to eq(105) }

let!(:tax) {calc.tax = 0.05}taxを呼びださないと、ブロックに渡した値が評価されないのでテストが失敗します。

しかし、let!とすることですぐさまインスタンス化したメソッドに対してブロックの中身を評価するので、メソッドを呼び出さなくてもテストは成功します。

stubについて

stubとは、もともとStant double(英語:スタントマン)の略らしいです。

テストの代役として、未実装でもテストに使えるメソッドを作るためのものと考えればいいでしょう。

作り方は、次の4ステップのようになっているイメージを持っているとわかりやすいかも。

  1. stubしたいものをdoubleの引数に渡す
  2. allowの引数にstubを渡し
  3. receiveの引数には無理やりくっつけたいメソッドをシンボルで渡す
  4. and_returnの引数に必ず返ってくる値を指定する
user = double('user') 
#=> userというstubを作り、

allow(user).to receive(:name).and_return('sato') 
#=> userスタブに対してnameというメソッドをsatoという値で返すようにする

あとはexpectでテストしたい内容を書いていけば良いですね。

# spec
expect(calc.add(5, 2, user.name)).to eq('7 by taguchi’)

# Calcクラス
class Calc

  def add(a, b, name)
    (a + b).to_s + ' by ' + name
  end

end

message expectationについて

Test doubleの一つ。

呼び出されなかった場合にテストが失敗する特性があるので、実装したい処理の前に必ず実行させたいメソッドがあった場合、その動作を保証させたい場合に使います。
(マイページに遷移する前に必ずログインさせるなど)

使い方は次の通り、

1.stubの時と同じように、doubleの引数に値を渡してtest doubleを作る

logger = double('logger’)

2.作成したtest_doubleが実行されたときには必ずrecieveの引数にシンボルで渡したメソッドが呼ばれるようにする

expect(logger).to receive(:log)
  1. クラスをインスタンス化する際に引数にtest_doubleを渡す
calc = Calc.new(logger)
  1. クラスのコンストラクタとしてinitializeを使ってtest_doubleで初期値を定義する
def initialize(logger)
    @logger = logger
end
  1. テストしたいメソッド内で、receiveの引数で渡したメソッドを呼ぶようにする
def add(a, b)
    @logger.log
    a + b
end

shared_exampleについて

共通化することで煩雑なspecファイルを整理したり、複数のspecファイルからコードの重複を減らすことで可読性や保守性を高めることができます。

railsの場合は,specディレクトリの下にsupportディレクトリを作成し、その配下に各shared_exampleを置くことで自動的に読み込まれるようになるので便利です。

Supportディレクトリ配下に置かれたファイルに命名規則はないので、別段ファイル名を_specとしなくてもOK。

Shared_exampleで定義したものはit_behaves_likeメソッドで呼び出すことができる。その際、第一引数に指定した名前のshared_exampleが展開されます。

RSpec.shared_examples "basic functions" do 
  it "can add"
  it "can subtrace"
  it "can multiply"
  it "can divide"
end

とshared_exampleを定義すれば、次のように呼び出せます。

RSpec.describe Calc do 
  context "normal mode” do 
    it_behaves_like “basic functions"
    # 以下のようにも書けるが、定義したものが上書きされることがあるのでit_behaves_likeを使う方が無難
    # include_examples "basic functions”
    # include_context "basic functions”
  end
end

ここまで色々と解説してきましたが、実際に実行してみないとわからないと思います!

ブラウザでコードが実行できるPaiza.ioを使ってぜひ色々テストを実行して見てくださいね。

参考リンク

include_examplesとit_behaves_likeの違い【RSpec】 - Qiita
要約include_context と include_examples はエイリアスの関係。動作は変わらない。include_(context|examples) は、現在のコンテキストに直接…
RSpec入門 (全15回) - プログラミングならドットインストール
BDDのためのテストフレームワークであるRSpecについて、その使い方を学んでいきます。
RSpecのletを使うのはどんなときか?(翻訳) - Qiita
はじめにRSpecにはletという機能があります。これを使うとインスタンス変数を次のように置き換えることができます。# インスタンス変数を使う場合before do @user = User…
Rspec3.1のbefore,afterフックが実行されるタイミング - Qiita
rspecの実行時にbefore、afterフックが実行されるタイミングがわからずにハマったので調べてみました。フックの種類:example,:context,:suiteの三種類。:exam…

ちょっとしたつぶやき

最近マーケティングの分野についても色々と勉強中です。

自分でサービスを作り上げる事で、起業して行ければなと思っています!