訳者: 桑田誠
この連載では、海外の良質な記事やブログを翻訳して紹介します。
第 1 回目は、Jared Carrollさんのブログ記事「RSpec Best Practice」です。
RSpec は、振舞駆動の設計プロセス (behaviour driven design process) において、人間にとって読みやすい仕様を書くための優れたツールです。RSpec で書かれた仕様が、アプリケーション開発における方向と検証を行います。本記事では私たちが気づいた、エレガントで保守しやすい仕様を書くためのプラクティスを紹介します。
定義するつもりのメソッドごとに、#describe を使うことから始めましょう。このとき引数にはメソッド名を指定します。クラスメソッドには名前の頭に「.」をつけ、インスタンスメソッドには頭に「#」をつけましょう。これは Ruby の標準的なドキュメントプラクティスに則っており、spec ランナーによる出力が読みやすくなります。
describe User do
describe '.authenticate' do
end
describe '.admins' do
end
describe '#admin?' do
end
describe '#name' do
end
end
メソッドの実行パスごとに、#context を書きましょう。与えられたコンテキストのもとでの〔訳注: つまり実行パスごとの〕メソッドの挙動を逐語的に指定します。
たとえば、次のメソッドには 2 つの実行パスがあります。
class SessionsController < ApplicationController
def create
user = User.authenticate :email => params[:email],
:password => params[:password]
if user.present?
session[:user_id] = user.id
redirect_to root_path
else
flash.now[:notice] = 'Invalid email and/or password'
render :new
end
end
end
対応するスペックでは 2 つのコンテキストを作成します。
describe '#create' do
context 'given valid credentials' do
end
context 'given invalid credentials' do
end
end
各 #context の引数における「given」の使い方に注意してください。「given」はメソッドに与えられる入力値を説明します。ほかに使える単語としては「when」があります。こちらは条件によって挙動が変わるときのコンテキストを表現するのに使えます。
describe '#destroy' do
context 'when logged in' do
end
context 'when not logged in' do
end
end
このスタイルに従えば、複数の #context を入れ子にすることで、より深い実行パスを明確に定義できます。
サンプル (example) 1 つにつき 1 つのエクスペクテーション (expectation) だけを記述するように努力してください。そうすればスペックの可読性が向上します。
次のスペックはひとつのサンプルのなかにあまり関連していないエクスペクテーションが混じっています。
describe UsersController do
describe '#create' do
...
it 'creates a new user' do
User.count.should == @count + 1
flash[:notice].should be
response.should redirect_to(user_path(assigns(:user)))
end
end
end
以下のようにエクスペクテーションを複数のサンプルに分解することで、挙動を明確に定義できますし、サンプルの保守もより簡単になります。
describe UsersController do
describe '#create' do
...
it 'creates a new user' do
User.count.should == @count + 1
end
it 'sets a flash message' do
flash[:notice].should be
end
it "redirects to the new user's profile" do
response.should redirect_to(user_path(assigns(:user)))
end
end
end
サンプルの説明は挙動を表す現在時制の動詞で始めてください。
it 'creates a new user' do
end
it 'sets a flash message' do
end
it 'redirects to the home page' do
end
it 'finds published posts' do
end
it 'enqueues a job' do
end
it 'raises an error' do
end
最後に、サンプルの説明を「should」で始めないようにしましょう。「should」を使うのは冗長ですし、結果としてスペックの出力が読みづらくなります。可読性が向上すると思うなら「the」や「a」や「an」は積極的にサンプルの説明に使っていきましょう。
「#it」や「#its」や「#specify」はタイプ量を減らすことができますが、可読性が犠牲になってしまいます。それらを使った場合、サンプルの中身を読まないと、それが何を意味しているのかを把握できなくなります。
次の例を使ってドキュメントフォーマッタ出力を比べてみましょう。
describe PostsController do
describe '#new' do
context 'when not logged in' do
...
subject do
response
end
it do
should redirect_to(sign_in_path)
end
its :body do
should match(/sign in/i)
end
end
end
end
明示的に振舞いを記述した場合は次のようになります。
describe PostsController do
describe '#new' do
context 'when not logged in' do
...
it 'redirects to the sign in page' do
response.should redirect_to(sign_in_path)
end
it 'displays a message to sign in' do
response.body.should match(/sign in/i)
end
end
end
end
前者の場合、そっけなくてコードっぽい感じで出力され、「should」が何度も登場して冗長です。
$ rspec spec/controllers/posts_controller_spec.rb --format documentation
PostsController
#new
when not logged in
should redirect to "/sign_in"
should match /sign in/i
後者の場合、スペックが非常に明確で読みやすく出力されます。
$ rspec spec/controllers/posts_controller_spec.rb --format documentation
PostsController
#new
when not logged in
redirects to the sign in page
displays a message to sign in
スペックを実行する際には常に「-format」オプションに「documentation」をつけるようにしましょう (RSpec 1.x では「-format」オプションは「nestable」と「specdoc」です)。
$ rspec spec/controllers/users_controller_spec.rb --format documentation
UsersController
#create
creates a new user
sets a flash message
redirects to the new user's profile
#show
finds the given user
displays its profile
#show.json
returns the given user as JSON
#destroy
deletes the given user
sets a flash message
redirects to the home page
出力が明瞭な会話のようになるまで、サンプルの名前を変更し続けましょう。
RSpec は役に立つマッチャを数多く揃えています。これらを使うと、スペックが話しことばにより近くなります (訳注: ここでの話しことばとは英語のことです)。
私たちが好んで使うマッチャを紹介します。使う前と使った後での違いは次のようになります。
# 使用前: 二重否定
object.should_not be_nil
# 使用後: 二重否定しない
object.should be
# 使用前: 'lambda' だと低水準すぎる
lambda { model.save! }.should raise_error(ActiveRecord::RecordNotFound)
# 使用後: 'expect' と 'to' でより自然なエクスペクテーションに
expect { model.save! }.to raise_error(ActiveRecord::RecordNotFound)
# 使用前: 実直に比較
collection.size.should == 4
# 使用後: サイズを調べるより高水準なエクスペクテーションを使う
collection.should have(4).items
ドキュメントを参照したり周りの人に聞いたりしてください。
すべてのブロックで、’do..end’ スタイルのブロックを使いましょう。このとき、たとえ 1 行のエクスペクテーションでも複数行のブロックを使います。可読性をより向上させるために、すべてのブロック間に空行を 1 行入れます。また、トップレベルの #describe では最初と最後に 1 行の空行を入れましょう。
比較してみます。
describe PostsController do
describe '#new' do
context 'when not logged in' do
...
subject { response }
it { should redirect_to(sign_in_path) }
its(:body) { should match(/sign in/i) }
end
end
end
分かりやすく構造化したコードは次のようになります。
describe PostsController do
describe '#new' do
context 'when not logged in' do
...
it 'redirects to the sign in page' do
response.should redirect_to(sign_in_path)
end
it 'displays a message to sign in' do
response.body.should match(/sign in/i)
end
end
end
end
複数の開発者がいるチームでは、一貫したフォーマットスタイルを維持するのは難しいことではありますが、各チームメイトのスタイルを視覚的にパースするのを修得するために費やされる時間を節約できるので、やる価値はあります。
本記事では、誰にとっても読みやすくて分かりやすい仕様を書くことに関するプラクティスを紹介しました。目標は、実行してパスするだけでなく、その出力結果がアプリケーションを完全に定義しているようなスペックを書くことです。ひとつひとつの小さなステップの積み重ねが、ゴールに向かうことを助けてくれます。私たちはもっと良い方法を引き続き学んでいるところです。あなたの RSpec ベストプラクティスはどのようなものでしょうか?
桑田誠。RSpecより「alias eq assert_equal」のほうが好き。