Ruby
RSpec
プロを目指す人のためのRuby入門

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

はじめに

この記事は書籍「プロを目指す人のための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回の記事で書き換えていきます。