53
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-06-06

はじめに

これは昨日公開した「テストコードの期待値は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、使うならどっち? / #kanrk06 // Speaker DeckKobito.QWjaif.png

RSpecユーザのためのMinitestチュートリアル

Everyday Rails - RSpecによるRailsテスト入門」の追加コンテンツとして提供している電子書籍です。
RSpecを使い慣れている人が「Minitestってどんなフレームワークなんだろう?」と思ったときに読むとちょうどいい内容になっています。

RSpecユーザのためのMinitestチュートリアルKobito.TYeYp5.png

53
52
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?