はじめに
これは昨日公開した「テストコードの期待値はDRYを捨ててベタ書きする ~テストコードの重要な役割とは?~ - Qiita」という記事のおまけ記事です。
元の記事はRSpecで書いたので「RSpecわからん」と思われた人が中にはいたようです。
そこで、RSpecではなくMinitestでテストコードを書いてみました。
また、「RSpecには共通化の機能があるんだからそれを使うべきだ」「itの中のアサーションは1つにすべきだ」という意見もいただいたので、より「RSpecらしい」書き方を考えてみました。
この記事ではこれら2パターンのテストコードを紹介します。
Minitestで書いたテストコード
元記事では以下のようなRSpecのコードを紹介しました。
describe Cloth do
describe '#half_price' do
it '半額の値段を計算する' do
cloth = Cloth.new('RSpec Tシャツ', 1000)
expect(cloth.half_price).to eq 500
cloth = Cloth.new('RSpec Tシャツ', 2000)
expect(cloth.half_price).to eq 1000
cloth = Cloth.new('RSpec Tシャツ', 999)
expect(cloth.half_price).to eq 499
end
end
end
これと同等のテストコードをMinitestで書くと以下のようになります。
class ClothTest < Minitest::Test
def test_half_price
cloth = Cloth.new('RSpec Tシャツ', 1000)
assert_equal 500, cloth.half_price
cloth = Cloth.new('RSpec Tシャツ', 2000)
assert_equal 1000, cloth.half_price
cloth = Cloth.new('RSpec Tシャツ', 999)
assert_equal 499, cloth.half_price
end
end
親クラスとメソッド名のルール
Minitestで書く場合は、Minitest::Test
を継承したテストクラスを作ります。
さらに、test_
で始まるpublicメソッドを作ります。
これがテスト実行時にMinitestから呼ばれるメソッドになります。
assert_equalの使い方
AがBに等しいことを検証する場合は assert_equal B, A
のように書きます。
期待値(expected)が第1引数に、実際の値(actual)が第2引数となる点に注意してください。
person_spec.rbをMinitestで書き直す
同じ要領でもう一つのテストコードも変換してみましょう。
describe Person do
describe '#format_date_of_birth' do
it '生年月日を和暦で表示すること' do
person = Person.new('Taro', Date.parse('1977-06-06'))
expect(person.format_date_of_birth).to eq '昭和52年6月6日'
person = Person.new('Taro', Date.parse('1988-06-06'))
expect(person.format_date_of_birth).to eq '昭和63年6月6日'
person = Person.new('Taro', Date.parse('1989-06-06'))
expect(person.format_date_of_birth).to eq '平成1年6月6日'
person = Person.new('Taro', Date.parse('2016-06-06'))
expect(person.format_date_of_birth).to eq '平成28年6月6日'
end
end
end
これはMinitestで書くとこうなります。
class PersonTest < Minitest::Test
def test_format_date_of_birth
person = Person.new('Taro', Date.parse('1977-06-06'))
assert_equal '昭和52年6月6日', person.format_date_of_birth
person = Person.new('Taro', Date.parse('1988-06-06'))
assert_equal '昭和63年6月6日', person.format_date_of_birth
person = Person.new('Taro', Date.parse('1989-06-06'))
assert_equal '平成1年6月6日', person.format_date_of_birth
person = Person.new('Taro', Date.parse('2016-06-06'))
assert_equal '平成28年6月6日', person.format_date_of_birth
end
end
コードの読み方はClothTest
の場合と同じなので省略します。
RSpecらしく書いたテストコード
RSpecにはdescribe/contextブロックをネストさせたり、letやsubjectといった機能を使ったりして、テストコードの重複を無くすことができます。
詳しい話はコードを見ながら説明した方がわかりやすいと思います。
これは元のテストコードです。
describe Cloth do
describe '#half_price' do
it '半額の値段を計算する' do
cloth = Cloth.new('RSpec Tシャツ', 1000)
expect(cloth.half_price).to eq 500
cloth = Cloth.new('RSpec Tシャツ', 2000)
expect(cloth.half_price).to eq 1000
cloth = Cloth.new('RSpec Tシャツ', 999)
expect(cloth.half_price).to eq 499
end
end
end
これをRSpecらしく書くとこうなります。
describe Cloth do
describe '#price_with_tax' do
let(:cloth) { Cloth.new('RSpec Tシャツ', price) }
subject { cloth.half_price }
context '割り切れる場合' do
let(:price) { 1000 }
it { is_expected.to eq 500 }
end
context '割り切れる場合・その2' do
let(:price) { 2000 }
it { is_expected.to eq 1000 }
end
context '端数が出る場合' do
let(:price) { 999 }
it { is_expected.to eq 499 }
end
end
end
describeとcontextについて
describeとcontextはそれぞれテストをグループ化するために使用します。
どちらも役割は全く同じ(エイリアスの関係)ですが、contextは「文脈」や「背景」といった意味なので、特にテスト条件を表す場合によく使われます。
subjectについて
subjectはテスト対象となる「実際の値」を明示的に示すために使います。
たとえば、subjectを使って書いた以下のコードは、
subject { cloth.half_price }
it { is_expected.to eq 500 }
以下のコードと同じ意味になります。
it 'xxx' do
actual = cloth.half_price
expect(actual).to eq 500
end
letについて
letは遅延評価されるローカル変数のようなものです。
たとえば、以下のようにlet(:price)
を宣言すると、
let(:cloth) { Cloth.new('RSpec Tシャツ', price) }
context '割り切れる場合' do
let(:price) { 1000 }
# ...
end
次のようなコードを書いたことと同じ意味になります。
let(:cloth) { Cloth.new('RSpec Tシャツ', 1000) }
context '割り切れる場合' do
# ...
end
また、let
はdescribe/contextブロックごとに異なる値を宣言できます。
なので、別のブロックのlet(:price)
で別の値を宣言した場合は、
let(:cloth) { Cloth.new('RSpec Tシャツ', price) }
# 他のcontextブロック...
context '端数が出る場合' do
let(:price) { 999 }
# ...
end
「割り切れる場合」と異なる値(この場合は999)がセットされることになります。
let(:cloth) { Cloth.new('RSpec Tシャツ', 999) }
# 他のcontextブロック...
context '端数が出る場合' do
# ...
end
itの長さについて
元のテストコードでは次のように、it
の中に複数のexpect( ).to eq
を書いていました。
it '半額の値段を計算する' do
cloth = Cloth.new('RSpec Tシャツ', 1000)
expect(cloth.half_price).to eq 500
cloth = Cloth.new('RSpec Tシャツ', 2000)
expect(cloth.half_price).to eq 1000
cloth = Cloth.new('RSpec Tシャツ', 999)
expect(cloth.half_price).to eq 499
end
しかし、これだと上から順番に実行されるため、途中でテストが失敗するとその先のテストは実行されません。
なので、3件中何件がパスして何件がパスしないのか実行結果から読み取ることができない、というデメリットがあります。
RSpecらしく書いたテストコードではcontextでテストをそれぞれグループ化しました。
context '割り切れる場合' do
let(:price) { 1000 }
it { is_expected.to eq 500 }
end
context '割り切れる場合・その2' do
let(:price) { 2000 }
it { is_expected.to eq 1000 }
end
context '端数が出る場合' do
let(:price) { 999 }
it { is_expected.to eq 499 }
end
こうするとそれぞれのit
は独立して実行されるので、3件中何件がパスして何件がパスしないのか確実に把握できます。
もっと詳しく
上で説明したRSpecの機能については以下の記事でさらに詳しく説明しています。
ピンと来なかった人はこちらを読んでみてください。
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita
person_spec.rbをRSpecらしく書く
もう一つのテストコード、person_spec.rb
もRSpecらしく書き直してみましょう。
変更前はこんなテストコードでした。
describe Person do
describe '#format_date_of_birth' do
it '生年月日を和暦で表示すること' do
person = Person.new('Taro', Date.parse('1977-06-06'))
expect(person.format_date_of_birth).to eq '昭和52年6月6日'
person = Person.new('Taro', Date.parse('1988-06-06'))
expect(person.format_date_of_birth).to eq '昭和63年6月6日'
person = Person.new('Taro', Date.parse('1989-06-06'))
expect(person.format_date_of_birth).to eq '平成1年6月6日'
person = Person.new('Taro', Date.parse('2016-06-06'))
expect(person.format_date_of_birth).to eq '平成28年6月6日'
end
end
end
これをRSpecらしく書き直すと次のようになります。
describe Person do
describe '#format_date_of_birth' do
let(:person) { Person.new('Taro', Date.parse(date)) }
subject { person.format_date_of_birth }
context '昭和生まれの場合' do
let(:date) { '1977-06-06' }
it { is_expected.to eq '昭和52年6月6日' }
end
context '昭和最終年生まれの場合' do
let(:date) { '1988-06-06' }
it { is_expected.to eq '昭和63年6月6日' }
end
context '平成元年生まれの場合' do
let(:date) { '1989-06-06' }
it { is_expected.to eq '平成1年6月6日' }
end
context '平成生まれの場合' do
let(:date) { '2016-06-06' }
it { is_expected.to eq '平成28年6月6日' }
end
end
end
ポイントはcloth_spec.rb
の場合と同じなので、説明は省略します。
まとめ
というわけで、この記事では「テストコードの期待値はDRYを捨ててベタ書きする」のテストコードをMinitestで書く場合と、RSpecらしく書く場合のサンプルコードを紹介しました。
みなさんがMinitestやRSpecを使ってテストコードを書く際の参考になれば幸いです。
あわせて読みたい
RSpecとMinitest、使うならどっち?
僕が関西Ruby会議06で発表した、MinitestとRSpecの違いを比較したスライドです。
「Minitestしかようわからん」「RSpecしかようわからん」という人が見ると参考になるかもしれません。
RSpecとMinitest、使うならどっち? / #kanrk06 // Speaker Deck
RSpecユーザのためのMinitestチュートリアル
「Everyday Rails - RSpecによるRailsテスト入門」の追加コンテンツとして提供している電子書籍です。
RSpecを使い慣れている人が「Minitestってどんなフレームワークなんだろう?」と思ったときに読むとちょうどいい内容になっています。