Edited at

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

More than 1 year has passed since last update.


はじめに

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

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

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

rubybook-mini.jpg


動画とソースコード

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

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

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

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

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

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

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


第7章 改札機プログラム


Minitest版


test/gate_test.rb

require 'minitest/autorun'

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

class GateTest < Minitest::Test
def setup
@umeda = Gate.new(:umeda)
@juso = Gate.new(:juso)
@mikuni = Gate.new(:mikuni)
end

def test_umeda_to_juso
ticket = Ticket.new(150)
@umeda.enter(ticket)
assert @juso.exit(ticket)
end

def test_umeda_to_mikuni_when_fare_is_not_enough
ticket = Ticket.new(150)
@umeda.enter(ticket)
refute @mikuni.exit(ticket)
end

def test_umeda_to_mikuni_when_fare_is_enough
ticket = Ticket.new(190)
@umeda.enter(ticket)
assert @mikuni.exit(ticket)
end

def test_juso_to_mikuni
ticket = Ticket.new(150)
@juso.enter(ticket)
assert @mikuni.exit(ticket)
end
end



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


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



ポイント


  • Minitestのsetupに相当する処理は、RSpecではbeforeブロックに記述します。

  • このテストコードではdescribeを使って「どの駅からどの駅まで乗るのか」をグループ分けしています。(例:describe 'Umeda to Juso' do

  • RSpecではdescribeだけでなく、contextを使ってグループ化することができます。両者の役割はまったく同じなのですが、contextは「テストの条件」を表す場合によく使われます。

  • ここではcontextを使って、「運賃が足りない場合(context 'fare is not enough' do)」と「足りる場合(context 'fare is enough' do)」をグループ分けしています。

  • 「検証する値が真か偽か」を確認する場合は、expect(A).to be_truthyexpect(A).to be_falseyという形式で検証します。


  • expect(A).to eq trueexpect(A).to be trueのような形で検証することもできますが、この場合は検証する値が必ず「trueそのものであること」が要求されます(つまり、1"abc"である場合はテストが失敗します)。

  • RSpecにおける真偽値の検証方法については以下の記事も参照してください。

参考:使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita


RSpec版(letを使う場合)


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) }

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



ポイント


  • 第1回の記事でも説明したとおり、テストコード内に登場するローカル変数やインスタンス変数はletを使って置き換えることができます。

  • ここでは@umeda@juso@mikuniという3つのインスタンス変数を、それぞれletに置き換えました。

  • インスタンス変数の代わりにletを使うと、typo(タイプミス)に気づきやすくなるというメリットがあります。

参考: RSpecのletを使うのはどんなときか?(翻訳) - Qiita


第8章 deep_freezeメソッド


Minitest版


test/deep_freezable_test.rb

require 'minitest/autorun'

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

class DeepFreezableTest < Minitest::Test
def test_deep_freeze_to_array
# 配列の値は正しいか?
assert_equal ['Japan', 'US', 'India'], Team::COUNTRIES
# 配列自身がfreezeされているか?
assert Team::COUNTRIES.frozen?
# 配列の要素がすべてfreezeされているか?
assert Team::COUNTRIES.all? { |country| country.frozen? }
end

def test_deep_freeze_to_hash
# ハッシュの値は正しいか?
assert_equal(
{ 'Japan' => 'yen', 'US' => 'dollar', 'India' => 'rupee' },
Bank::CURRENCIES
)
# ハッシュ自身がfreezeされているか?
assert Bank::CURRENCIES.frozen?
# ハッシュの要素(キーと値)がすべてfreezeされているか?
assert Bank::CURRENCIES.all? { |key, value| key.frozen? && value.frozen? }
end
end



RSpec版


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']
# 配列自身がfreezeされているか?
expect(Team::COUNTRIES).to be_frozen
# 配列の要素がすべてfreezeされているか?
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' })
# ハッシュ自身がfreezeされているか?
expect(Bank::CURRENCIES).to be_frozen
# ハッシュの要素(キーと値)がすべてfreezeされているか?
expect(Bank::CURRENCIES.all? { |key, value| key.frozen? && value.frozen? }).to be_truthy
end
end
end



ポイント


  • RSpecにはpredicateマッチャという機能があります。これはfrozen?のような?で終わるメソッドを、be_frozenのような形で検証できる機能です。

  • 具体的にいうと、以下の2つのテストコードはまったく同じ内容を検証しています。

# predicateマッチャを使わない場合

expect(Team::COUNTRIES.frozen?).to be_truthy

# predicateマッチャを使う場合
expect(Team::COUNTRIES).to be_frozen


まとめ

この記事では「プロを目指す人のためのRuby入門」の第7章と第8章までのテストコードをRSpecに書き換えました。

第10章(注:第9章はテストコードがありません)のテストコードは第3回の記事で書き換えていきます。