RSpec のすごいところ

(注: 以下の内容は、RSpec ユーザの間で広まっていることでもなく、もちろん RSpec 開発チームの公式な見解でもなく、あくまでワシの個人的な見解です。)


RSpec のすごいところは、コードに対してではなく仕様に対してテストを書くことを明確にしたことだと思う。何を今さらと言われそうだけど、今さらになってようやく気づいたニワトリ頭ですまんかった。

ワシも最初は、「assert_equal(expected, actual)」のかわりに「actual.should == expected」と書くかっこよさに目を奪われて、テストコードを自然言語に近い形で記述するのが RSpec のすごいところだと勘違いしてたし、それが「TDD (Test Driven Development)」から「BDD (Behaviour Driven Development)」へという新しい潮流だと勘違いしてた。


そう、これらはまったくの勘違いだった。別に assert_equal() が .should == になったところで、それは見た目が変わっただけで実質的な変化は何もない。確かに見た目は自然言語に近くなるけど、それがどうした?自然言語に近いと顧客が読んでくれるか?そんなのはまったくの幻想だ。顧客が読むのは Excel や World の資料であって、*_spec.rb なんて読むわけがない。テストコードを読むのも書くのも結局はプログラマだけなんだから、プログラマにとって読みやすく書きやすければよく、自然言語に近いかどうかはさほど重要ではない。「Behaviour」なんて用語にいたってはちっとも重要ではない。

それよりも RSpec で重要なのは、まず describe() と it() で仕様を記述し、それに対してテストコードを書くということが、自然に表現されていることだ。Test::Unit での TestCase や test_xxx() は、単にテストコードを分類していただけにすぎない。これに対し、RSpec の describe() や it() は、テストコードを分類しているのではなく、仕様を記述する役割を果たしている。そして、RSpec では describe() や it() に書いた仕様に対して、テストコードを書くことになる。

## これは単にテストを分類しているだけ
class NumericTest < Test::Unit::TestCase
  def test_plus
    assert_equal 2, 1+1
  end
  def test_minus
    assert_equal 0, 1-1
  end
end

## これは仕様に対してテストを書いている
describe Numeric, '#+' do
  it "2つの数を足した結果を返す" do
    (1+1).should == 2
  end
end
describe Numeric, '#-' do
  it "ある数から別の数を引いた結果を返す" do
    (1-1).should == 0
  end
end

結局、テストコードは何をもとにして書くのか、ということだよね。Test::Unit を使ってた頃は、テストコードはソースコードに対して書くものだと思ってた。でもそれは間違いで、テストコードは仕様に対して書くものということなんだ。これは Test::Unit を使ってたままでは分からなかったと思う。

RSpec 信者の中には、「RSpec のコードはテストコードではない、動く仕様書である」という人もいるけど、別にそこまでドラスティックな考えにしなくてもいいように思う (してもいいけど)。確かに「動く仕様書」というのはカッコイイ考え方だけど、自然言語で書いた部分とRubyコードで書いた部分のどちらも仕様と呼ぶのは混乱を招くように思う。そこまでしなくても「describe() と it() で仕様を表し、それに対してテストコードを書く」という説明で十分わかりやすいと感じている。

その観点からいうと、describe() と it() という名前はよくなかった。もっと直接的に specs_of() と spec() という名前にしたほうが「仕様を記述している」ことが明確になるし、無理に it を主語にしなくてもよくなる。

### DSLチックなこの書き方よりも…
describe Numeric, '#+' do
  it "2つの数を足した結果を返す" do
    (1+1).should == 2
  end
end

### こっちの方が「仕様を記述するんだ」ということが明確になり、
### かつ無理に it を主語にしなくて済むので好き
specs_of Numeric, '#+' do
  spec "2つの数を足した結果を返す" do
    (1+1).should == 2
  end
end

この書き方をしようとしたら、alias specs_of describe; alias spec it; とすればいいだけ…なんだけど、結構試行錯誤が必要だった。RSpec のソースはわかりづらい。

require 'rubygems'
require 'spec'

## describe のかわりに specs_of を、また it のかわりに spec を使う
Spec::DSL::Main.module_eval { alias specs_of describe }
Spec::Example::ExampleGroupMethods.module_eval { alias spec it }

Ruby 1.9 から付属する minispec であれば、次のようにすればいい。

require 'minitest/spec'
require 'minitest/autorun'

Kernel.module_eval { alias specs_of describe }
class <<MiniTest::Spec; alias spec it; end

specs_of 'Numeric#+' do    # instead of describe()
  spec "合計を返す" do     # instead of it "..."
    assert_equal 2, 1+1
  end
end


というわけで、テストを書くときはソースコードに対して書くのではなく、仕様に対して書くことを意識しようと思う (2010 年にもなってようやく気づくというのが、いかにテストを苦手としているのかがわかるよね :)。


今日のまとめ: テストコードは仕様に対して書く


あわせて読みたい (マジで参考になるのでおすすめ):