きっかけ
「プロを目指す人のためのRuby入門」を買いました。評判に違わず素晴らしい本です。しかし、業務ではRSpecを使うのにMinitestのコードを書いても自分のためにならないと思いRSpecで書き換えることにしました。これをみてRSpecの書き方がわからない人が少しでも慣れてくれれば嬉しいです。
そもそも、プロを目指すというタイトルなのに、テストコードがテスティングフレームワークのデファクトスタンダートであるRSpecではないのは何故なのだろうと疑問に思いました。その疑問に答えるヒントが、著者の伊藤惇一さんのスライドに書かれていました。
21ページ、xUnit形式(Minitest)とSpec形式(RSpec)の対比にて、
- xUnit形式は
Rubyの言語がフルに利用できる
- Spec形式は
DSLに制約されやすい
と書かれています。つまり独自言語色の強いRSpecは、チェリー本のテーマであるピュアなRubyの入門からは多少外れた位置あると考えられます。
追記(2018.1.27)
コメント欄にて、著者の伊藤惇一さんよりMinitestを選んだ理由を教えていただきました。以下引用です。
- RSpecはgemインストールの手順が必要になるので、初心者のハードルやトラブル発生のリスクが上がってしまう
- RSpecはDSLを使うので、DSLの説明が必要になる。これも初心者のハードルを上げる要因になる
- RSpecは現場レベルのそこそこ複雑なテストを書くときには便利だが、本書ぐらいシンプルなテストであればMinitestでも十分
- Railsチュートリアルも最近はMinitestを使っているので、Railsチュートリアル経験者にも馴染みやすい
- xUnit形式はJUnitのような他のテスティングフレームワークでも考え方が共通しているので、他言語経験者も理解しやすい(他言語でもSpec形式のフレームワークは増えてきていますが)
前準備
RSpecを入れましょう。そのあとで、MinitestとRSpecのテストを分けるために、spec
フォルダを作りましょう。
gem install rspec mkdir spec
実践
第3章 FizzBuzzブログラム
このコードをテストします。lib/
のコードはチェリー本からの引用です。引用をなるべく減らそうと思ったのですが、説明する上でテスト対象のコードは残した方が理解しやすいと思い、そのまま引用しています。
lib/fizz_buzz.rb
def fizz_buzz(n) if n % 15 == 0 "Fizz Buzz" elsif n % 3 == 0 "Fizz" elsif n % 5 == 0 "Buzz" else n.to_s end end
これをRSpecで書き直しましょう。ファイル名はfizz_buzz_spec.rb
にしましょう。
touch spec/fizz_buzz_spec.rb
これがRSpecのコードはこちらです。describe
ブロックでテストをグループ化しましょう。ここではtest_fizz_buzz
というfizz_buzz
メソッドをテストすることを宣言しています。さらにcontext
ブロックでテストで確認したい項目ごとに分けています。このテストでは、以下の4つに分けています。
it
ブロックで期待している動作を明文化します。it
ブロックの中のexpect
は、Minitestでいうassert_equal
とほぼ同様の動きをします。このテストのみMinitestで書かれたコードも引用します。
test/fizz_buzz_test.rb(チェリー本より引用)
require 'minitest/autorun' require_relative '../lib/fizz_buzz' class FizzBuzzTest < Minitest::Test def test_fizz_buzz assert_equal '1', fizz_buzz(1) assert_equal '2', fizz_buzz(2) assert_equal 'Fizz', fizz_buzz(3) assert_equal '4', fizz_buzz(4) assert_equal 'Buzz', fizz_buzz(5) assert_equal 'Fizz', fizz_buzz(6) assert_equal 'Fizz Buzz', fizz_buzz(15) end end
spec/fizz_buzz_spec.rb
require_relative '../lib/fizz_buzz' describe 'fizz_buzz' do context '3または5の倍数ではない数字が引数の場合' do it '引数が返却される' do expect(fizz_buzz(1)).to eq('1') expect(fizz_buzz(2)).to eq('2') expect(fizz_buzz(4)).to eq('4') end end context '3の倍数が引数の場合' do it 'Fizzが返却される' do expect(fizz_buzz(3)).to eq('Fizz') expect(fizz_buzz(6)).to eq('Fizz') end end context '5の倍数が引数の場合' do it 'Buzzが返却される' do expect(fizz_buzz(5)).to eq('Buzz') end end context '3及び5の倍数が引数の場合' do it 'Fizz Buzzが返却される' do expect(fizz_buzz(15)).to eq('Fizz Buzz') end end end
実行します。
$ rspec spec/fizz_buzz_spec.rb .... Finished in 0.00374 seconds (files took 0.10134 seconds to load) 4 examples, 0 failures
オールグリーンですね!RSpecの詳しい構文はチェリー本の著者が書いた記事がわかりやすいです。
第4章 RGBカラー変換プログラム
同じように「第4章 RGBカラー変換プログラム」もRSpecに書き換えましょう。最初にテスト対象のコードです。
lib/rgb.rb
def to_hex(r, g, b) [r, g, b].inject('#') do |hex, n| hex + n.to_s(16).rjust(2, '0') end end def to_ints(hex) hex.scan(/\w\w/).map(&:hex) end
次にRSpecです。ファイル作成は省略します。
spec/rgb_spec.rb
require_relative '../lib/rgb' describe 'to_hex' do context 'r,g,bそれぞれの値が同じ値' do it '16進数に変換されたカラーコードが返却される' do expect(to_hex(0, 0, 0)).to eq('#000000') expect(to_hex(255, 255, 255)).to eq('#ffffff') end end context 'r,g,bそれぞれの値が違う値' do it '16進数に変換されたカラーコードが返却される' do expect(to_hex(4, 60, 120)).to eq('#043c78') end end end describe 'to_ints' do context 'r,g,bそれぞれの値が同じ値' do it '10進数に変換されたカラーコードが返却される' do expect(to_ints('#000000')).to eq([0, 0, 0]) expect(to_ints('#ffffff')).to eq([255, 255, 255]) end end context 'r,g,bそれぞれの値が同じ値' do it '10進数に変換されたカラーコードが返却される' do expect(to_ints('#043c78')).to eq([4, 60, 120]) end end end
ここでは、to_hex
とto_ints
がそれぞれtrue
になることが確認できるようにメソッドごとにdescribe
ブロックを分けています。早速実行してみましょう。
$ rspec spec/rgb_spec.rb .... Finished in 0.00517 seconds (files took 0.10886 seconds to load) 4 examples, 0 failures
第5章 長さの単位変換プログラム
lib/convert_length.rb
UNITS = {m: 1.0, ft: 3.28, in: 39.37} def convert_length(length, from: :m, to: :m) (length / UNITS[from] * UNITS[to]).round(2) end
spec/convert_length_spec.rb
require_relative '../lib/convert_length' describe 'convert_length' do context 'fromがメートル、toがインチの場合' do it 'メートルからインチに変換できる' do expect(convert_length(1, from: :m, to: :in)).to eq(39.37) end end context 'fromがインチ、toがメートルの場合' do it 'インチからメートルに変換できる' do expect(convert_length(15, from: :in, to: :m)).to eq(0.38) end end context 'fromがメートル、toがインチの場合' do it 'フィートからメートルに変換できる' do expect(convert_length(35000, from: :ft, to: :m)).to eq(10670.73) end end end
どれ単位からどの単位の変換をテストしたいかをcontext
ブロックを分けています。実行してみましょう。
$ rspec spec/convert_length_spec.rb ... Finished in 0.00338 seconds (files took 0.13897 seconds to load) 3 examples, 0 failures
第6章 ハッシュ記法変換プログラム
lib/convert_hash_syntax.rb
def convert_hash_syntax(old_syntax) old_syntax.gsub(/:(\w+) *=> */, '\1: ') end
spec/convert_hash_syntax_spec.rb
require_relative '../lib/convert_hash_syntax' describe 'convert_hash_syntax' do old_syntax = <<~TEXT { :name => 'Alice', :age => 20, :gender => :female } TEXT expected = <<~TEXT { name: 'Alice', age: 20, gender: :female } TEXT it '=>が:に置き換わる' do expect(convert_hash_syntax(old_syntax)).to eq(expected) end end
追記(2018.1.27)
チェリー本著者である伊藤惇一さんよりコメントで指摘があり、describe
直下にローカル変数を書くとスコープ範囲が広くなりテストが複雑化した時に可読性が落ちます。したがって、上記のコードはよくない例としてください。it
ブロックの中でローカル変数を書いてスコープ範囲を狭めましょう。
require_relative '../lib/convert_hash_syntax' # example(it)内でローカル変数を宣言する describe 'convert_hash_syntax' do it '=>が:に置き換わる' do old_syntax = <<~TEXT { :name => 'Alice', :age => 20, :gender => :female } TEXT expected = <<~TEXT { name: 'Alice', age: 20, gender: :female } TEXT expect(convert_hash_syntax(old_syntax)).to eq(expected) end end
実行しましょう。
$ rspec spec/convert_hash_syntax_spec.rb . Finished in 0.00273 seconds (files took 0.1079 seconds to load) 1 example, 0 failures
最後に
RSpecはDSL
なので、最初は読み書きしにくいです。この続きではもっと慣れて書いてる記事をお見せしたいです。それでは。