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

## はじめに

この記事は書籍「[プロを目指す人のためのRuby入門](http://gihyo.jp/book/2017/978-4-7741-9397-7)」のテストコードを、筆者自らRSpecに書き換える連載記事の第2回です。
第1回を読んでいない方は、先に第1回を読んでからこの記事に戻ってきてください。

- [【動画付き】「プロを目指す人のためのRuby入門」のテストコードをRSpecに書き換える・その1 \- Qiita](https://qiita.com/jnchito/items/60ea1389d00e72b729e7)

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

[![rubybook-mini.jpg](https://qiita-image-store.s3.amazonaws.com/0/7465/975c1ed6-1815-1d08-4e3d-8fa720f1a78f.jpeg "rubybook-mini.jpg")](http://gihyo.jp/book/2017/978-4-7741-9397-7)

### 動画とソースコード
この記事の内容は動画（スクリーンキャスト）形式で解説しています。
細かい説明は口頭で話しているので、ぜひ動画もチェックしてください。
（録音状況が悪いため、ときどき大きなノイズが入ります。ごめんなさい🙏）

[「プロを目指す人のためのRuby入門」のテストコードをRSpecに書き換える・その2<img width="475" alt="Screen Shot 2018-03-19 at 5.18.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/7465/38769548-9f28-8896-6323-626e0ddb48d2.png">](https://youtu.be/2-7YJb9YH_s)

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

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

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

## 第7章 改札機プログラム

### Minitest版

```ruby: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を使わない場合）

```ruby: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_truthy`や`expect(A).to be_falsey`という形式で検証します。
- `expect(A).to eq true`や`expect(A).to be true`のような形で検証することもできますが、この場合は検証する値が必ず「`true`そのものであること」が要求されます（つまり、`1`や`"abc"`である場合はテストが失敗します）。
- RSpecにおける真偽値の検証方法については以下の記事も参照してください。

参考：[使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 \- Qiita](https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5)

### RSpec版（letを使う場合）

```ruby: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](https://qiita.com/jnchito/items/cdd9eef2ed193267c651)

## 第8章 deep_freezeメソッド

### Minitest版

```ruby: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版

```ruby: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つのテストコードはまったく同じ内容を検証しています。

```ruby
# predicateマッチャを使わない場合
expect(Team::COUNTRIES.frozen?).to be_truthy

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

## まとめ

この記事では「[プロを目指す人のためのRuby入門](http://gihyo.jp/book/2017/978-4-7741-9397-7)」の第7章と第8章までのテストコードをRSpecに書き換えました。

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

- [【動画付き】「プロを目指す人のためのRuby入門」のテストコードをRSpecに書き換える・その3（最終回） \- Qiita](https://qiita.com/jnchito/items/54394440f91b665c870e)