きっかけ
「プロを目指す人のためのRuby入門」を買いました。評判に違わず素晴らしい本です。しかし、業務ではRSpecを使うのにMinitestのコードを書いても自分のためにならないと思いRSpecで書き換えることにしました。これをみてRSpecの書き方がわからない人が少しでも慣れてくれれば嬉しいです。
そもそも、プロを目指すというタイトルなのに、テストコードがテスティングフレームワークのデファクトスタンダートであるRSpecではないのは何故なのだろうと疑問に思いました。その疑問に答えるヒントが、著者の伊藤惇一さんのスライドに書かれていました。
21ページ、xUnit形式(Minitest)とSpec形式(RSpec)の対比にて、
- xUnit形式は
Rubyの言語がフルに利用できる
- Spec形式は
DSLに制約されやすい
と書かれています。つまり独自言語色の強いRSpecは、チェリー本のテーマであるピュアなRubyの入門からは多少外れた位置あると考えられます。
追記(2018.1.27)
コメント欄にて、著者の伊藤惇一さんよりMinitestを選んだ理由を教えていただきました。以下引用です。
前準備
RSpecを入れましょう。そのあとで、MinitestとRSpecのテストを分けるために、spec
フォルダを作りましょう。
gem install rspec
mkdir spec
実践
このコードをテストします。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つに分けています。
- 3の倍数を引数にしたら、Fizzが返却される
- 5の倍数を引数にしたら、Buzzが返却される
- 3と5の倍数を引数にしたら、Fizz Buzzが返却される
- 上記以外だと引数が返却される
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の詳しい構文はチェリー本の著者が書いた記事がわかりやすいです。
qiita.com
第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'
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
なので、最初は読み書きしにくいです。この続きではもっと慣れて書いてる記事をお見せしたいです。それでは。