RSpec

RSpec2.14からRSpec3に対応するときにわかったこと

More than 3 years have passed since last update.


所感


  • 全体的にテストのスコープをより大事にするようになったような印象

  • なのでテストを書く側もちゃんと意識するべき

  • 明示的に書くことを推奨するようになったような印象

  • やっぱりexpectをつかたほうが健全

コマンドラインオプションもいくつか整理されています。


DocumentationFormatterの'spec', 's'廃止

rspec -cfs /path/to/file_or_dir

rspec -fs /path/to/file_or_dir
rspec -f spec /path/to/file_or_dir

などとして使っていた場合はそれぞれ

rspec -cfd /path/to/file_or_dir

rspec -fd /path/to/file_or_dir
rspec -f doc /path/to/file_or_dir

のように指定するように変わりました.


--line-number, -l オプションの廃止

指定した行付近のexampleを実行するには

rspec -l 10 /path/to/file

の代わりに以下のように指定するようになりました.

rspec /path/to/file:10


described_classはネストされた方のクラスを参照する

class Hoge

end

class Moge
end

describe Hoge do
it 'ここでのdescribed_classはHogeクラス' do
expect(described_class).to eq(Hoge)
end

describe Moge do
it 'RSpec2ではHogeクラス、3ではMogeクラスになる' do
expect(described_class).to eq(Hoge)
end
end

context '例えば以下のようにテストを生成したとき' do
sample_id = [100, 999, 1, 5]

sample_id.each do |id|
context id do
it 'RSpec2ではこれでGreen, RSpec3ではFixnumなのでRed' do
expect(described_class).to eq(Hoge)
end
end
end
end
end

RSpec2.14での実行結果


RSpec2.14

Finished in 0.00311 seconds

6 examples, 0 failures

RSpec3での実行結果

Failures:

1) Hoge Moge RSpec2ではHogeクラス、3ではMogeクラスになる
Failure/Error: expect(described_class).to eq(Hoge)

expected: Hoge
got: Moge

(compared using ==)

Diff:
@@ -1,2 +1,2 @@
-Hoge
+Moge
# ./sample_spec.rb:14:in `block (3 levels) in <top (required)>'

2) Hoge 例えば以下のようにテストを生成したとき 100 RSpec2ではこれでGreen, RSpec3ではFixnumなのでRed
Failure/Error: expect(described_class).to eq(Hoge)

expected: Hoge
got: 100

(compared using ==)
# ./sample_spec.rb:24:in `block (5 levels) in <top (required)>'

3) Hoge 例えば以下のようにテストを生成したとき 999 RSpec2ではこれでGreen, RSpec3ではFixnumなのでRed
Failure/Error: expect(described_class).to eq(Hoge)

expected: Hoge
got: 999

(compared using ==)

※中略※

Finished in 0.00209 seconds (files took 0.09767 seconds to load)
6 examples, 5 failures


be_true, be_falseはbe_truthy, be_falsyに

RSpec2でのbe_true, be_falseはtruthyかどうか、falsyかどうかを検査しているだけでした。

つまりRSpec2では


RSpec2

describe 'be_true, be_false matcher' do

it 'Stringインスタンスはtruthy' do
expect('hogeee').to be_true
end
it 'nilはfalsy' do
expect(nil).to be_false
end
end

2 examples, 0 failures

となるので非常に紛らわしいです。

ので、RSpec3からはbe_truthy, be_falsyと書くように変更になりました。


RSpec3

describe 'be_true, be_falseはbe_truthy, be_falsyに' do

it 'Stringインスタンスはtruthy' do
expect('hogeee').to be_truthy
end
it 'nilはfalsy' do
expect(nil).to be_falsy
end
end

2 examples, 0 failures


true, falseのチェックはbe true, be falseで

it 'trueのチェック' do

expect(true).to be true
end
it 'falseのチェック' do
expect(false).to be false
end

2 examples, 0 failures


before :all, after :all ブロック内でlet, subjectで宣言した識別子を使うとエラー

let, subjectは各itブロックごとに初期化されるものなので、そもそもbefore :allafter :allなどのすべてのテストのスコープにまたがった部分では使われるべきではありません。

RSpec3ではそこがより厳密になりました。

describe 'letのスコープをより厳密に' do

let (:hoge) { 'hogemoge' }

before :all do
hoge
end

it { expect(hoge).to eq('hogemoge') }
end

describe 'subjectのスコープをより厳密に' do
subject { 'hogemoge' }

before :all do
subject
end

it { is_expected.to eq('hogemoge') }
end

上記のテストコードはRSpec3では以下の様な出力になります。

letのスコープをより厳密に

example at ./sample_spec.rb:8 (FAILED - 1)

subjectのスコープをより厳密に
example at ./sample_spec.rb:18 (FAILED - 2)

Failures:

1) letのスコープをより厳密に
Failure/Error: hoge
RuntimeError:
let declaration `hoge` accessed in a `before(:context)` hook at:
/path/to/sample_spec.rb:5:in `block (2 levels) in <top (required)>'

`let` and `subject` declarations are not intended to be called
in a `before(:context)` hook, as they exist to define state that
is reset between each example, while `before(:context)` exists to
define state that is shared across examples in an example group.
# ./sample_spec.rb:5:in `block (2 levels) in <top (required)>'

2) subjectのスコープをより厳密に
Failure/Error: subject
RuntimeError:
subject accessed in a `before(:context)` hook at:
/path/to/sample_spec.rb:15:in `block (2 levels) in <top (required)>'

`let` and `subject` declarations are not intended to be called
in a `before(:context)` hook, as they exist to define state that
is reset between each example, while `before(:context)` exists to
define state that is shared across examples in an example group.
# ./sample_spec.rb:15:in `block (2 levels) in <top (required)>'

Finished in 0.00048 seconds (files took 0.09662 seconds to load)
2 examples, 2 failures

Failed examples:

rspec ./sample_spec.rb:8 # letのスコープをより厳密に
rspec ./sample_spec.rb:18 # subjectのスコープをより厳密に

RSpec2.14で実行するときは、上記のis_expected.toshouldに書き換えて実行してみてください。下記のような出力になるはずです。

letのスコープをより厳密に

should eq "hogemoge"

subjectのスコープをより厳密に
should eq "hogemoge"

Finished in 0.00088 seconds
2 examples, 0 failures

たちが悪いのは、after :allフックでのみこのエラーが出た場合、テストの実行自体はGreenになってしまうことです。

下記のようにテストコードのbeforeafterに書き換えて実行すると。。。

describe 'letのスコープをより厳密に' do

let (:hoge) { 'hogemoge' }

after :all do
hoge
end

it { expect(hoge).to eq('hogemoge') }
end

describe 'subjectのスコープをより厳密に' do
subject { 'hogemoge' }

after :all do
subject
end

it { is_expected.to eq('hogemoge') }
end

letのスコープをより厳密に

should eq "hogemoge"

An error occurred in an `after(:context)` hook.
RuntimeError: let declaration `hoge` accessed in an `after(:context)` hook at:
/path/to/sample_spec.rb:5:in `block (2 levels) in <top (required)>'

`let` and `subject` declarations are not intended to be called
in an `after(:context)` hook, as they exist to define state that
is reset between each example, while `after(:context)` exists to
cleanup state that is shared across examples in an example group.

occurred at /path/to/rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rspec-core-3.1.3/lib/rspec/core/memoized_helpers.rb:155:in `fetch'

subjectのスコープをより厳密に
should eq "hogemoge"

An error occurred in an `after(:context)` hook.
RuntimeError: subject accessed in an `after(:context)` hook at:
/path/to/sample_spec.rb:15:in `block (2 levels) in <top (required)>'

`let` and `subject` declarations are not intended to be called
in an `after(:context)` hook, as they exist to define state that
is reset between each example, while `after(:context)` exists to
cleanup state that is shared across examples in an example group.

occurred at /path/to/rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rspec-core-3.1.3/lib/rspec/core/memoized_helpers.rb:155:in `fetch'

Finished in 0.00124 seconds (files took 0.09662 seconds to load)
2 examples, 0 failures

0 failures...

テストは全部Greenですね。。。


引数がないことのテストは明示的にno_argsを使う

テストコード

describe '引数をとらないことをテストするとき明示的にno_argsを使う' do

it 'no_args使うと意図したテストになる' do
expect(Array).to receive(:new).with(no_args)
Array.new
end
it 'no_args使わなかったらエラー' do
expect(Array).to receive(:new).with()
Array.new
end
end

実行結果

引数をとらないことをテストするとき明示的にno_argsを使う

no_args使うと意図したテストになる
no_args使わなかったらエラー (FAILED - 1)

Failures:

1) 引数をとらないことをテストするとき明示的にno_argsを使う no_args使わなかったらエラー
Failure/Error: expect(Array).to receive(:new).with()
ArgumentError:
`with` must have at least one argument. Use `no_args` matcher to set the expectation of receiving no arguments.
# ./sample_spec.rb:7:in `block (2 levels) in <top (required)>'

Finished in 0.00569 seconds (files took 0.0962 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./qiita_spec.rb:6 # 引数をとらないことをテストするとき明示的にno_argsを使う no_args使わなかったらエラー


shouldメソッドにmatcherが渡されなかったときテストが成功しているように見えてしまう

例えばエクスペクテーションの行が長すぎて折り返したいとき、間違ったところで折り返してしまうとテストがスルーされてしまいます。

そもそもRSpec3を使っていればshouldを使うとDeprecation Warningが出るんですが、これが2.14を使っているときはとても気づきにくくなっています。

テストコード

describe 'shouldの罠' do

context 'エクスペクテーションの行が長いとき' do
it '間違ったところで折り返すとテストがスルーされる' do
tooooooooooooo_loooooooooooooooong_valueeeeeee = 'hoge'
tooooooooooooo_loooooooooooooooong_expected_valueeeeeeeee = 'hoge'
tooooooooooooo_loooooooooooooooong_valueeeeeee.should
eq('mogeeeee')
end

it 'expectを使うとちゃんとエラーが出る' do
tooooooooooooo_loooooooooooooooong_valueeeeeee = 'hoge'
tooooooooooooo_loooooooooooooooong_expected_valueeeeeeeee = 'hoge'
expect(tooooooooooooo_loooooooooooooooong_valueeeeeee).to
eq('mogeeeee')
end
end
end

実行結果(RSpec2.14のとき)

shouldの罠

エクスペクテーションの行が長いとき
間違ったところで折り返すとテストがスルーされる
expectを使うとちゃんとエラーが出る (FAILED - 1)

Failures:

1) shouldの罠 エクスペクテーションの行が長いとき expectを使うとちゃんとエラーが出る
Failure/Error: expect(tooooooooooooo_loooooooooooooooong_valueeeeeee).to
ArgumentError:
The expect syntax does not support operator matchers, so you must pass a matcher to `#to`.
# ./qiita_spec.rb:13:in `block (3 levels) in <top (required)>'

Finished in 0.0008 seconds
2 examples, 1 failure

Failed examples:

rspec ./qiita_spec.rb:10 # shouldの罠 エクスペクテーションの行が長いとき expectを使うとちゃんとエラーが出る

一つ目のテストは成功しているように見えてしまいますが、意図した比較はできていません。

expectを使うとちゃんとtoに引数を渡してくださいとおこられます。


公式のアナウンスを見たい方はこちら

http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3

日本語訳は以下

http://nilp.hatenablog.com/entry/2014/05/28/003335