Edited at

【動画付き】「プロを目指す人のためのRuby入門」のテストコードをRSpecに書き換える・その3(最終回)

More than 1 year has passed since last update.


はじめに

この記事は書籍「プロを目指す人のためのRuby入門」のテストコードを、筆者自らRSpecに書き換える連載記事の第3回(最終回)です。

第1回と第2回を読んでいない方は、先にそちらを読んでからこの記事に戻ってきてください。

今回は第10章のテストコードをRSpecに書き換えていきます。

rubybook-mini.jpg


動画とソースコード

この記事の内容は動画(スクリーンキャスト)形式で解説しています。

細かい説明は口頭で話しているので、ぜひ動画もチェックしてください。

(録音状況が悪いため、ときどき大きなノイズが入ります。ごめんなさい🙏)

「プロを目指す人のためのRuby入門」のテストコードをRSpecに書き換える・その3(最終回)Screen Shot 2018-03-19 at 5.20.16.png

また、ソースコードはこちらにアップしています。

https://github.com/JunichiIto/ruby-book-codes/tree/rspec/ruby-book

それでは以下が本編です。


第10章 ワードシンセサイザー・Effects編


Minitest版


test/effects_test.rb

require 'minitest/autorun'

require './lib/effects'

class EffectsTest < Minitest::Test
def test_reverse
effect = Effects.reverse
assert_equal 'ybuR si !nuf', effect.call('Ruby is fun!')
end

def test_echo
effect = Effects.echo(2)
assert_equal 'RRuubbyy iiss ffuunn!!', effect.call('Ruby is fun!')

effect = Effects.echo(3)
assert_equal 'RRRuuubbbyyy iiisss fffuuunnn!!!', effect.call('Ruby is fun!')
end

def test_loud
effect = Effects.loud(2)
assert_equal 'RUBY!! IS!! FUN!!!', effect.call('Ruby is fun!')

effect = Effects.loud(3)
assert_equal 'RUBY!!! IS!!! FUN!!!!', effect.call('Ruby is fun!')
end
end



RSpec版(letやsubjectを使わない場合)


spec/effects_spec.rb

require './spec/spec_helper'

require './lib/effects'

RSpec.describe Effects do
describe '.reverse' do
it 'returns valid string' do
effect = Effects.reverse
expect(effect.call('Ruby is fun!')).to eq 'ybuR si !nuf'
end
end

describe '.echo' do
it 'returns valid string' do
effect = Effects.echo(2)
expect(effect.call('Ruby is fun!')).to eq 'RRuubbyy iiss ffuunn!!'

effect = Effects.echo(3)
expect(effect.call('Ruby is fun!')).to eq 'RRRuuubbbyyy iiisss fffuuunnn!!!'
end
end

describe '.loud' do
it 'returns valid string' do
effect = Effects.loud(2)
expect(effect.call('Ruby is fun!')).to eq 'RUBY!! IS!! FUN!!!'

effect = Effects.loud(3)
expect(effect.call('Ruby is fun!')).to eq 'RUBY!!! IS!!! FUN!!!!'
end
end
end



ポイント


  • 冒頭のRSpec.describe Effects doでは、'Effects'のような文字列ではなく、Effectsというモジュール自身を渡しています。このように、describeにはテスト対象の「クラスそのもの」や「モジュールそのもの」を渡すこともよくあります。


RSpec版(letやsubjectを使う場合)


spec/effects_spec.rb

require './spec/spec_helper'

require './lib/effects'

RSpec.describe Effects do
subject { effect.call('Ruby is fun!') }

describe '.reverse' do
let(:effect) { Effects.reverse }
it { is_expected.to eq 'ybuR si !nuf' }
end

describe '.echo' do
context 'rate is 2' do
let(:effect) { Effects.echo(2) }
it { is_expected.to eq 'RRuubbyy iiss ffuunn!!' }
end
context 'rate is 3' do
let(:effect) { Effects.echo(3) }
it { is_expected.to eq 'RRRuuubbbyyy iiisss fffuuunnn!!!' }
end
end

describe '.loud' do
context 'level is 2' do
let(:effect) { Effects.loud(2) }
it { is_expected.to eq 'RUBY!! IS!! FUN!!!' }
end
context 'level is 3' do
let(:effect) { Effects.loud(3) }
it { is_expected.to eq 'RUBY!!! IS!!! FUN!!!!' }
end
end
end



ポイント



  • describecontextブロックの中で、検証したい値がどれも同じだった場合はsubjectとして切り出すことができます。


  • subjectに切り出すと、it { is_expected.to eq '...' }のように、ワンライナー形式で書くことができます。これはit { expect(subject).to eq '...' }のように書いたのと同じ意味になります。


  • letsubjectは遅延評価されます。つまり、上から順に呼び出されるのではなく、その値が必要になったタイミングで初めて呼び出されます。

  • そのため、subject { effect.call('Ruby is fun!') }effectは、describecontext内で定義されたlet(:effect)の値に応じて、Effects.reverseになったり、Effects.echo(2)になったりします。


  • letsubjectの遅延評価については、以下の記事でも詳しく説明しているので、よくわからない場合はこちらの記事をご覧ください。

参考:使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita


第10章 ワードシンセサイザー・WordSynth編


Minitest版


test/word_synth_test.rb

require 'minitest/autorun'

require './lib/word_synth'
require './lib/effects'

class WordSynthTest < Minitest::Test
def test_play_without_effects
synth = WordSynth.new
assert_equal 'Ruby is fun!', synth.play('Ruby is fun!')
end

def test_play_with_reverse
synth = WordSynth.new
synth.add_effect(Effects.reverse)
assert_equal 'ybuR si !nuf', synth.play('Ruby is fun!')
end

def test_play_with_many_effects
synth = WordSynth.new
synth.add_effect(Effects.echo(2))
synth.add_effect(Effects.loud(3))
synth.add_effect(Effects.reverse)
assert_equal '!!!YYBBUURR !!!SSII !!!!!NNUUFF', synth.play('Ruby is fun!')
end
end



RSpec版(letやsubjectを使わない場合)


spec/word_synth_spec.rb

require './spec/spec_helper'

require './lib/word_synth'
require './lib/effects'

RSpec.describe WordSynth do
context 'without effects' do
it 'returns modified string' do
synth = WordSynth.new
expect(synth.play('Ruby is fun!')).to eq 'Ruby is fun!'
end
end

context 'with reverse' do
it 'returns modified string' do
synth = WordSynth.new
synth.add_effect(Effects.reverse)
expect(synth.play('Ruby is fun!')).to eq 'ybuR si !nuf'
end
end

context 'with many effects' do
it 'returns modified string' do
synth = WordSynth.new
synth.add_effect(Effects.echo(2))
synth.add_effect(Effects.loud(3))
synth.add_effect(Effects.reverse)
expect(synth.play('Ruby is fun!')).to eq '!!!YYBBUURR !!!SSII !!!!!NNUUFF'
end
end
end



ポイント

(ここでは特に新しいトピックはありません。)


RSpec版(letやsubjectを使う場合)


spec/word_synth_spec.rb

require './spec/spec_helper'

require './lib/word_synth'
require './lib/effects'

RSpec.describe WordSynth do
let(:synth) { WordSynth.new }
subject { synth.play('Ruby is fun!') }

context 'without effects' do
it { is_expected.to eq 'Ruby is fun!' }
end

context 'with reverse' do
before do
synth.add_effect(Effects.reverse)
end
it { is_expected.to eq 'ybuR si !nuf' }
end

context 'with many effects' do
before do
synth.add_effect(Effects.echo(2))
synth.add_effect(Effects.loud(3))
synth.add_effect(Effects.reverse)
end
it { is_expected.to eq '!!!YYBBUURR !!!SSII !!!!!NNUUFF' }
end
end




  • beforeブロックはdescribecontextごとに用意することもできます。


  • describecontextの外側にもbeforeブロックがあった場合は、beforeブロックは「外のブロック → 中のブロック」の順で呼ばれます。

  • たとえば、下のようなコードがあった場合、describe 'foo' do内に書かれたテストでは「before 1 → before 2」の順でbeforeが呼ばれ、describe 'bar' do内に書かれたテストでは「before 1 → before 3」の順でbeforeが呼ばれます。

RSpec.describe Something do

before do
# before 1
end
describe 'foo' do
before do
# before 2
end
end
describe 'bar' do
before do
# before 3
end
end
end


議論:letやsubjectはどこまで駆使すべきか

letsubjectを駆使すると、いわゆる「RSpecらしいテストコード」になります。

また、重複がなくなり、テストコードがDRYになります。

一方で、RSpecにある程度精通していないと、何をやっているのかよくわからないテストコードに見えてしまうデメリットもあります。

また、「letsubjectをうまく切り出す方法」を編み出すのに、やたら工数がかかってしまうこともよくあります。

こうしたデメリットは「RSpecは難しい」「RSpecはよくわからない」と、RSpec初心者を遠ざける原因になると僕は考えています。

個人的には、無理にletsubjectを駆使する必要はなく、「さらっと書けて、さらっと読めるテストコード」を採用しても全然構わないと考えています。

参考:サヨナラBetter Specs!? 雑で気楽なRSpecのススメ - Qiita


おまけ:第7章と第8章のテストでletやsubjectを駆使してみる

letsubjectを駆使する必要はない」とか言いつつ、letsubjectを駆使したコードもそれはそれで面白いので、参考までに第7章と第8章のテストコードを書き換えてみます。

みなさんは変更前と変更後、どっちのコードが好きですか?(そして、どっちのコードが読みやすいでしょうか?)


第7章・変更前(letやsubjectを使わない場合)


spec/gate_spec.rb

require './spec/spec_helper'

require './lib/gate'
require './lib/ticket'

RSpec.describe 'Gate' do
before do
@umeda = Gate.new(:umeda)
@juso = Gate.new(:juso)
@mikuni = Gate.new(:mikuni)
end

describe 'Umeda to Juso' do
it 'is OK' do
ticket = Ticket.new(150)
@umeda.enter(ticket)
expect(@juso.exit(ticket)).to be_truthy
end
end

describe 'Umeda to Mikuni' do
context 'fare is not enough' do
it 'is NG' do
ticket = Ticket.new(150)
@umeda.enter(ticket)
expect(@mikuni.exit(ticket)).to be_falsey
end
end
context 'fare is enough' do
it 'is OK' do
ticket = Ticket.new(190)
@umeda.enter(ticket)
expect(@mikuni.exit(ticket)).to be_truthy
end
end
end

describe 'Juso to Mikuni' do
it 'is OK' do
ticket = Ticket.new(150)
@juso.enter(ticket)
expect(@mikuni.exit(ticket)).to be_truthy
end
end
end



第7章・変更後(letやsubjectを駆使した場合)


spec/gate_spec.rb

require './spec/spec_helper'

require './lib/gate'
require './lib/ticket'

RSpec.describe 'Gate' do
let(:umeda) { Gate.new(:umeda) }
let(:juso) { Gate.new(:juso) }
let(:mikuni) { Gate.new(:mikuni) }
let(:ticket) { Ticket.new(fare) }
before do
station_from.enter(ticket)
end
subject { station_to.exit(ticket) }

describe 'Umeda to Juso' do
let(:station_from) { umeda }
let(:station_to) { juso }
let(:fare) { 150 }
it { is_expected.to be_truthy }
end

describe 'Umeda to Mikuni' do
let(:station_from) { umeda }
let(:station_to) { mikuni }
context 'fare is not enough' do
let(:fare) { 150 }
it { is_expected.to be_falsey }
end
context 'fare is enough' do
let(:fare) { 190 }
it { is_expected.to be_truthy }
end
end

describe 'Juso to Mikuni' do
let(:station_from) { juso }
let(:station_to) { mikuni }
let(:fare) { 150 }
it { is_expected.to be_truthy }
end
end



第8章・変更前(letやsubjectを使わない場合)


spec/deep_freezable_spec.rb

require './spec/spec_helper'

require './lib/bank'
require './lib/team'

RSpec.describe 'Deep freezable' do
describe 'to array' do
it 'freezes deeply' do
expect(Team::COUNTRIES).to eq ['Japan', 'US', 'India']
expect(Team::COUNTRIES).to be_frozen
expect(Team::COUNTRIES.all? { |country| country.frozen? }).to be_truthy
end
end

describe 'to hash' do
it 'freezes deeply' do
expect(Bank::CURRENCIES).to eq({ 'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee' })
expect(Bank::CURRENCIES).to be_frozen
expect(Bank::CURRENCIES.all? { |key, value| key.frozen? && value.frozen? }).to be_truthy
end
end
end



第8章・変更後(letやsubjectを駆使した場合)

ここではletsubjectだけでなく、allマッチャコンポーザブルマッチャも使っています。


spec/deep_freezable_spec.rb

require './spec/spec_helper'

require './lib/bank'
require './lib/team'

RSpec.describe 'Deep freezable' do
describe 'to array' do
subject { Team::COUNTRIES }
it { is_expected.to eq ['Japan', 'US', 'India'] }
it { is_expected.to be_frozen }
it { is_expected.to all be_frozen }
end

describe 'to hash' do
subject { Bank::CURRENCIES }
it { is_expected.to eq({ 'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee' }) }
it { is_expected.to be_frozen }
it { is_expected.to all match [be_frozen, be_frozen] }
end
end



まとめ

というわけで、この連載記事では書籍「プロを目指す人のためのRuby入門」のテストコードをMinitestからRSpecに書き換える方法を説明してみました。

「Minitestの次はRSpecをマスターしたい!」と考えている方の参考になれば幸いです😃


あわせて読みたい


RSpecの基礎

RSpecにはまだまだ多くの機能が用意されています。

実務でよく使う機能については、僕が昔に書いた以下の連載記事をご覧ください。


RSpecの公式ドキュメント

より詳細に調べたい場合は、RSpecの公式ドキュメントをご覧ください。


RailsアプリケーションをRSpecでテストする

RailsアプリケーションをRSpecでテストする場合は、僕が翻訳した電子書籍「Everyday Rails - RSpecによるRailsテスト入門」がお勧めです。

RSpecの基本はこの本から学ぶこともできます。

「Everyday Rails - RSpecによるRailsテスト入門」の内容については、以下のブログ記事をご覧ください。