Help us understand the problem. What is going on with this article?

【動画付き】「プロを目指す人のための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テスト入門」の内容については、以下のブログ記事をご覧ください。

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした