Rails3 Rspec で(いい加減な)ユニットテストを書いてみる

まじめにrailsにおけるBDDをやろうと、簡単なアプリを使って、BDD開発してみようかと思います。

Rspec をより便利に/テストしやすくするツールは山ほどあるのですが(guard-spec とか fabrication とか spork とか)、一度にいろいろやろうとするとややこしいので最低限の書き方だけを押さえます。

WEB+DB PRESS vol.61 Rails3テスト最前線 を参考(というかほとんど習って)にしています。

非常に分かりやすい記事だったので、少し古いですが入手してみるとよいです。

造るアプリ

メインはツーエンドツーテストを書くことなので、scaffold でジェネレートしたものをそのまま用います。

下準備

とりあえず rails をセットアップします。

mkdir end2end_test
cd end2end_test
# Gemfile
source :rubygems
gem 'rails'
bundle install --path=vendor/bundle

ruby 1.9.x と bundler は入ってる前提で。

細かいバージョンは違ってもあまり関係ないと思うので、そのときの最新版でやります。

bundle exec rails new . -T

-T は Testunit を除くオプションです。

Rspec を用いる予定なので、デフォルトのテストフレームワークは不要なのです。

Gemfile の上書き確認がでるので yes して上書きします。

ココまでは各自自由な方法で rails アプリを new してください。

Rspec 追記

Rspec の gem を Gemfile に追加します。

gem 'rspec'
gem 'rspec-rails'

適宜 group で囲んでください。そこまでキチンとしなくてもよいですが。

bundle exec rails g rspec:install

上記コマンドで Rspec の設定ファイルを生成します。

これで、新しいコンロトーラーやモデルをジェネレートした場合に、自動的にひな形となる Spec を生成してくれます。

scaffold

データベースは sqlite で済ませます。mysql 等が良い人は適宜環境と database.yml を書き換えてください。

bundle exec rails g scaffold employee name:string
bundle exec rake db:migrate

従業員テーブルで名前フィールドを持たせました。それだけのテーブルです。

ジェネレートが完了したので、サーバを立ち上げて、アクセスできるか試してみます(やらなくてもいいです)

http://localhost:3000/employees

一通りのCRUD操作はできますね。相変わらずすばらしいですね。

さて、この場合、ある程度の実装は済んでしまっているので、完全なテストファーストとは言いがたいですが、scaffold によって生成されたこのアプリについてテストを書いて行きます。

幾つか制約を組み込むことで新機能についてはテストファーストな開発として取り込んでみます。

テストの実行

scaffold でテストケースのテンプレが生成されているので、なんのテストにもなりませんが、テストケースを実行してみます。

bundle exec rake spec

spec タスクで spec/ 以下のテストケースが実行されます。

生成されたテンプレートのテストが一通り動きます。何も無いので成功(緑)か延期(黄色)しかでません。

ほかにも、モデルだけやコントローラーだけなど搾ったタスクもあります。

モデルのテスト

一番簡単なモデルのテストから書きます。

このモデルの仕様として、”名前は必須項目である” という制約をもうけ、それをふまえて spec/models/employee_spec.rb  を以下のように書いてみました。

# coding: utf-8require 'spec_helper'
describe Employee do
  context 'when a user create,' do
    before do
      @employee = Employee.new(
        :name => '室見立華'
      )
    end

    it 'should be able to save.' do
      @employee.save.should be_true
      @employee.should be_persisted
    end

    it 'should be invalid' do      
      @employee.name = nil
      @employee.save.should be_false
      @employee.should_not be_persisted
    end
  end
end

すこしづつ見て行くと、まず context で囲ったブロックがあります。

これは単なる名前付けというだけで、ユーザーの作成のテストとしてひとまとめに区切るのに利用しました。

必須では無いので、なくてもよいですが、ほかのテストを書いて行ったときに分かりにくくなるので適宜区切ると良いと思います。

ちなみに、contextとdescribeというメソッドが使えますが違いはありません。

次の階層には、before という処理が入っています。その名の通り、各テストケースが動く前に行われる処理を before で定義できます。

ここでは、 新しい Employee インスタンスを作って、name に名前をセットしました。

次に it ‘…’ do; endというようなブロックがありますが、コレがテストケースです。

テストの内容が分かるような説明を書き、テスト処理を記述して行きます。

一つ目に正常系のテストケースとして、before で用意した @employee にsave を実行した時に true が返る、つまりバリデーションエラー無しに保存がされたという事をチェックしています。

rspec では matcher というメソッドを用いて、オブジェクトの状態を確認し、それが条件を満たさない場合をテストの失敗としています。

ここでは、@employee.save == true と @employee.persisted? == true を満たさなければならないことを表現しています。

その次の異常系のテストケースとして、一度名前を nil に設定して、保存に失敗する事とを検証しています。

これらのテストケースを記述して、モデルのテストを実行します。

bundle exec rake spec:models
Failures:
1) Employee when a user create, should be invalid
 Failure/Error: @employee.save.should be_false
 expected: false value
 got: true
 # ./spec/models/employee_spec.rb:20:in `block (3 levels) in <top (required)>'
Finished in 0.08819 seconds
 2 examples, 1 failure

二つ目のテストが失敗しました。

名前が空の場合は失敗しなければいけない(saveがfalseを返さなければならない)のに、保存できてしまったからです

このテストが通るようにモデルを実装するのが BDD(TDD?) です。

名前のフィールドに必須のバリデーションを実装します。

class Employee < ActiveRecord::Base
    validates_presence_of :name # required name
    attr_accessible :name
end

それでは再度実行してみます。

bundle exec rake spec:models
Finished in 0.09949 seconds
 2 examples, 0 failures

エラーがなくなり、きちんと名前が空の場合は保存に失敗しました。

もし他にも制約やメソッドを実装したい場合は、先に望む結果をテストコードに実装し、エラーを解消するように実装して行けばいいわけですね。

コントローラーのテスト

モデル単位でのテストが完了したら、コントローラーのテストをつくる所なのですが、scaffold を使ったためか、scaffold のテストコードがすでに生成されていました。

(Rails, Rspec のバージョンによっては結果が異なるかもしれません)

なので、コードリーディングをしてみます。

describe "GET index" do
  it "assigns all employees as @employees" do
    employee = Employee.create! valid_attributes
    get :index, {}, valid_session
    assigns(:employees).should eq([employee])
  end
end

“GET index” と書かれていますがコレはただの文字列です。

index アクションに GET リクエストをすると分かりやすく表現しています。

一行目で新しく employee を作っています。

valid_attributes はファイルの前の方に記述されています。

def valid_attributes
   { :name => "姪乃浜梢" }
 end

空のハッシュを返すように実装されていましたが、先ほどモデルに名前が必須である条件を足したので、ここで名前を返すように書き換えました。

create! なのでこの時点で1レコード保存されます。

二行目で index に GET でアクセスします。

ここで、index アクションの中身が実行されます。

get メソッドは、アクション名、パラメータ、セッション を受け取ります。

ココでは indexアクションを呼ぶために、:index を、パラメータは空なので空ハッシュを、セッションは valid_session が返す値を使うように書いています。

valid_session もファイル前方に記述されていました。

使い回したいセッションの値がある場合はココに書くとよさそうです。

三行目で @employees が最初に作った employees を含んだ配列と一致するかを検証しています。

@employees という変数を使っているなら、assigns(:employees) というように書きます。

このようなコードで、index アクションのテストが行われていました。

index アクションの手前でレコードを作っているのがなんだか微妙な気もしますが、before を使って、アクションのテスト前にレコードを用意するなどできそうです。

他のアクションも同様の流れで記述されています。更新系はちょっと複雑でした。

例えば新規作成の “POST create” では

it "creates a new Employee" do
  expect {
    post :create, {:employee => valid_attributes}, valid_session
  }.to change(Employee, :count).by(1)
end

expect {  }.to というブロックがありますが、コレは、 expect に渡したブロック内での処理の結果、何が起るべきかを to に渡しています。

上記の場合、expect 内で create アクションに POST して、その結果、 Employee.count が 1 だけ上昇(増加)していること、つまり create アクションで正しい値を POST したら新しくユーザーが1つできていることのテストの記述でした。

ビューのテスト

index, new, edit, show それぞれの画面のテストがすでに生成されています。まずは index を見てみます。

describe "employees/index" do
  before(:each) do
    assign(:employees, [
      stub_model(Employee,
        :name => "Name"
      ), 
      stub_model(Employee,
        :name => "Name"
      )
    ])
  end
  it "renders a list of employees" do
    render
    # Run the generator again with the --webrat flag if you want to use webrat matchers
    assert_select "tr>td", :text => "Name".to_s, :count => 2
  end
end

before はモデルのテストでも使ったように、it の前に実行されます。

ここでは :each を引数で渡しています。これは、it ごとに before を実行する意味になりますが、引数を渡さなかった場合も同じです。

他に、:all を渡すと、最初に一回だけ実行され、it ごとには実行されなくなります。

before では前処理として、assign に渡している変数から何となく分かるように、@employees に 二つの Employee のインスタンスをセットするような処理が書かれています。

(コレを @employee に代入するように書き換えてもテストは通りました。assign との違いはなんでしょう?ビュー用の変数とテストコード中で使う変数なのかを明確にするために別の方法でやるというようなことでしょうか)

stub_model メソッドはその名の通り、渡されたモデルクラスのスタブインスタンスを返してくれます。

コレはデータベースを経由しないので、データベースが用意されていなくても動きます。

使い方も見ての通りで、クラス名とそれを初期化する値を渡すだけです。

スタブですが、@employees に二人の従業員が割り当てられた状態が再現され、index ビューがレンダーされたときのテストが実行されます。

一行目の render メソッドはこのテスト対象のビューを表示しています。

これでHTMLとして出力されたことが再現されます。

二行目の assert_select は、HTMLセレクタで要素を絞り、その要素の内容について :text => “Name”.to_s であるか、要素の数について :count => 2 であるかどうかを検証しています。

index.html.erb ではテーブル組でレコードを表現しているので、before で @employees に割り当てた各データが td 要素にきちんとデータが表示されているか、そしてそれが割り当てた二件分表示されているかを検証しています。

この場合、二件とも同じ “Name” という名前なので、画面内の td 要素で、内容が “Name” ものが二件存在する、というテストなので、少し現実ばなれしているようにも見えます。

ビュー単体レベルだからコレくらいざっくりでもいい、ということなんでしょうか?そう思えばそんな気もします。

MVC のユニットテスト

一通り、テストコードがどんなものかを見てみました。

慣れないとちょっと戸惑いそうですが、いつもの ruby コードが書けるわけですし、そうゆう意味ではそれほどハードルは高くは無い気がしてきました。

これらはユニット(単体)テストと呼ばれ、他のコンポーネントと結合度合いが少ない方です。

ココがしっかりしていれば、バグの発生もかなり押さえられるのではないでしょうか。

次は capybara という gem を用いてエンドツーエンドテストを行います。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする