Edited at

「テストコードの期待値はDRYを捨ててベタ書きする」のテストコードをMinitestで書く・RSpecらしく書く

More than 3 years have passed since last update.


はじめに

これは昨日公開した「テストコードの期待値はDRYを捨ててベタ書きする ~テストコードの重要な役割とは?~ - Qiita」という記事のおまけ記事です。

元の記事はRSpecで書いたので「RSpecわからん」と思われた人が中にはいたようです。

そこで、RSpecではなくMinitestでテストコードを書いてみました。

また、「RSpecには共通化の機能があるんだからそれを使うべきだ」「itの中のアサーションは1つにすべきだ」という意見もいただいたので、より「RSpecらしい」書き方を考えてみました。

この記事ではこれら2パターンのテストコードを紹介します。


Minitestで書いたテストコード

元記事では以下のようなRSpecのコードを紹介しました。


cloth_spec.rb

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で書くと以下のようになります。


cloth_test.rb

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で書き直す

同じ要領でもう一つのテストコードも変換してみましょう。


person_spec.rb

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で書くとこうなります。


person_test.rb

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といった機能を使ったりして、テストコードの重複を無くすことができます。

詳しい話はコードを見ながら説明した方がわかりやすいと思います。

これは元のテストコードです。


cloth_spec.rb

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らしく書くとこうなります。


cloth_spec.rb

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らしく書き直してみましょう。

変更前はこんなテストコードでした。


person_spec.rb

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らしく書き直すと次のようになります。


person_spec.rb

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チュートリアル

Everyday Rails - RSpecによるRailsテスト入門」の追加コンテンツとして提供している電子書籍です。

RSpecを使い慣れている人が「Minitestってどんなフレームワークなんだろう?」と思ったときに読むとちょうどいい内容になっています。