この記事は、下記の記事「【前編】「プロを目指す人のためのRuby入門」(チェリー本)のMinitest部分をRSpecに書き換える」の続きです。前編と同じようにテストをRSpecで書いていきます。前編との変更点としてピュアなRubyでRSecのテストを書くのはなく、RSpecらしく書くことを目標にしました。理由は、DRYしやすいためです。
第7章 改札機プログラムの作成
前編と同じようにテスト対象のプログラムとRSpecのコードを記述します。**なおlib/
フォルダのプログラムは全てチェリー本の引用です
lib/gate.rb
# 改札機を表すクラス class Gate STATIONS = [:umeda, :juso, :mikuni] #:nodoc: FARES = [150, 190] #:nodoc: # Gateオブジェクトの作成 # ==== 引数 # *+name+ - 駅名 def initialize(name) @name = name end # 改札機に通って駅に入場する # ==== 引数 # *+ticket+ - 切符 def enter(ticket) ticket.stamp(@name) end # 改札機を通って駅から出場する # ==== 引数 # *+ticket+ - 切符 # ==== 戻り値 # *+boolean+ - 運賃が足りて入れば+true+、不足して入れば+false+ def exit(ticket) fare = calc_fare(ticket) fare <= ticket.fare end # 運賃を計算する # ==== 引数 # *+ticket+ - 切符 def calc_fare(ticket) from = STATIONS.index(ticket.stamped_at) to = STATIONS.index(@name) distance = to - from FARES[distance - 1] end end
lib/ticket.rb
class Ticket attr_reader :fare, :stamped_at def initialize(fare) @fare = fare end def stamp(name) @stamped_at = name end end
spec/gate_spec.rb
require_relative '../lib/gate' require_relative '../lib/ticket' describe 'gate' do let(:umeda) { Gate.new(:umeda) } let(:juso) { Gate.new(:juso) } let(:mikuni) { Gate.new(:mikuni) } let(:ticket) { Ticket.new(fare) } describe '梅田から' do before do umeda.enter(ticket) end context '十三まで' do let(:fare) { 150 } it { expect(juso.exit(ticket)).to be_truthy } end context '三国まで' do context '運賃が足りている' do let(:fare) { 190 } it { expect(mikuni.exit(ticket)).to be_truthy } end context '運賃が足りていない' do let(:fare) { 150 } it { expect(mikuni.exit(ticket)).to be_falsey } end end end context '十三から三国まで' do let(:fare) { 150 } before do juso.enter(ticket) end it { expect(mikuni.exit(ticket)).to be_truthy } end end
前編と比べて、RSpecのコードが複雑になってきています。梅田から
他の駅で降車する処理は、3つあるのでdescribe
でblockにしてあります。梅田駅に入場する処理はbefore
blockの中で処理できるよう共通化しています。let
を使って共通部分は変数にしています。let
の遅延展開については下記のページが参考になります。boolean
の判定は、be_truthy
,be_falsey
でmatchさせるようにしています。。
実行結果は、後編は省略します。
第8章 deep_freezeメソッドの作成
lib/bank.rb
require_relative '../lib/deep_freezable' class Bank extend DeepFreezable CURRENCIES = deep_freeze( { 'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee' } ) end
lib/team.rb
require_relative '../lib/deep_freezable' class Team extend DeepFreezable COUNTRIES = deep_freeze(['Japan', 'US', 'India']) end
lib/deep_freezable.rb
module DeepFreezable def deep_freeze(array_or_hash) case array_or_hash when Array array_or_hash.each do |element| element.freeze end when Hash array_or_hash.each do |key, value| key.freeze value.freeze end end array_or_hash.freeze end end
spec/deep_freezable_spec.rb
require_relative '../lib/bank' require_relative '../lib/team' describe 'deep_freeze_to_array' do context '配列の値は正しいか?' do it '正しい' do expect(Team::COUNTRIES).to eq(['Japan', 'US', 'India']) end end context '配列自身がfreezeされているか?' do it 'freezeされている' do expect(Team::COUNTRIES.frozen?).to be_truthy end end context '配列の要素が全てfreezeされているか?' do it 'freezeされている' do expect(Team::COUNTRIES.all? { |country| country.frozen? }).to be_truthy end end end describe 'deep_freeze_to_hash' do context 'ハッシュの値は正しいか?' do let(:currencies) { { 'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee' } } it '正しい' do expect(Bank::CURRENCIES).to eq(currencies) end end context 'ハッシュ自身かfreezeされているか?' do it 'freezeされている' do expect(Bank::CURRENCIES.frozen?).to be_truthy end end context 'ハッシュの要素(キーと値)が全てfreezeされているか?' do it '全てfreezeされている' do expect(Bank::CURRENCIES.all? { |key, value| key.frozen? && value.frozen? }).to be_truthy end end end
ここは今まで通り書いています。特筆する箇所もないでしょう。
第10章 ワードシンセサイザーの作成
Effectクラス
この章は、テストが2つあるのでそれぞれ見ていきましょう。
lib/effects.rb
module Effects def self.reverse ->(words) do words.split(' ').map(&:reverse).join(' ') end end def self.echo(rate) ->(words) do # スペースならそのまま返す # スペース以外ならその文字を指定された回数だけ繰り返す words.chars.map { |c| c == ' ' ? c : c * rate }.join end end def self.loud(level) ->(words) do # スペースで分割 > 大文字変換と"!"の付与 > スペースで連結 words.split(' ').map { |word| word.upcase + '!' * level}.join(' ') end end end
spec/effects_spec.rb
require_relative '../lib/effects' describe 'reverse' do let(:effect) { Effects.reverse } subject(:effect_reverse) { effect.call('Ruby is fun!') } it '文字列が反転する' do expect(effect_reverse).to eq('ybuR si !nuf') end end describe 'echo' do let(:effect) { Effects.echo(arg) } subject(:effect_echo) { effect.call('Ruby is fun!') } context '引数が2の場合' do let(:arg) { 2 } it '各文字が2回ずつ繰り返される' do expect(effect_echo).to eq('RRuubbyy iiss ffuunn!!') end end context '引数が3の場合' do let(:arg) { 3 } it '各文字が3回ずつ繰り返される' do expect(effect_echo).to eq('RRRuuubbbyyy iiisss fffuuunnn!!!') end end end describe 'loud' do let(:effect) { Effects.loud(arg) } subject(:effect_loud) { effect.call('Ruby is fun!') } context '引数が2の場合' do let(:arg) { 2 } it '文字列が大文字になり、単語毎の末尾に`!!`がつく' do expect(effect_loud).to eq('RUBY!! IS!! FUN!!!') end end context '引数が3の場合' do let(:arg) { 3 } it '文字列が大文字になり、単語毎の末尾に`!!!`がつく' do expect(effect_loud).to eq('RUBY!!! IS!!! FUN!!!!') end end end
describe
ごとにexpect
で実行するメソッドと引数は同じなので、新しくsubject
を使用しています。subject
はexpect
の引数として使用するのが一般的です。
WordSynthクラス
lib/word_synth.rb
class WordSynth def initialize @effects = [] end def add_effect(effect) @effects << effect end def play(original_words) @effects.inject(original_words) do |words, effect| effect.call(words) end end end
spec/word_synth_spec.rb
require_relative '../lib/word_synth' require_relative '../lib/effects' describe 'play' do let(:synth) { WordSynth.new } subject(:play) { synth.play('Ruby is fun!') } context 'effectなし' do it 'エフェクトも付与される' do expect(play).to eq('Ruby is fun!') end end context 'reverseエフェクトを加える' do let!(:add_effect) { synth.add_effect(Effects.reverse) } it '文字列が反転する' do expect(play).to eq('ybuR si !nuf') end end context '多くのエフェクトを加える' do let!(:add_effect1) { synth.add_effect(Effects.echo(2)) } let!(:add_effect2) { synth.add_effect(Effects.loud(3)) } let!(:add_effect3) { synth.add_effect(Effects.reverse) } it '文字列が反転する' do expect(play).to eq('!!!YYBBUURR !!!SSII !!!!!NNUUFF') end end end
ここではlet!
を新しく使用しています。let!
はit
blockに入る直前に実行されます。let!
の後にlet
が評価されます。
最後に
今回はピュアRuby風に書くのではなく、RSpecに則ってDRYな書き方にしました。subject
やlet
の違い、before
やlet!
の実行タイミングなど、実際に書き直してみると新しい発見がありました。本当はshared_context
など使用して、共通化したかったのですが、技量が足りず実現には至らなかったです。今後もRSpecのコードをネタにできる素材があったら、記事を書きます。