モデルのテスト
2013/08/18
今回は、Ruby on Rails のモデルクラスに対するテストの書き方を解説します。
Customer
モデルの仕様
この連載のサンプルアプリケーション Sinope
がいったい何を目的とするものなのかまだ決まっていませんが、仮に「顧客(customer)」という概念が必要で、そのために Customer
という名前のモデルを用意するものとしましょう。
このモデルの主な仕様は以下の通りです:
- 姓(family_name)、名(given_name)、姓フリガナ(family_name_kana)、名フリガナ(given_name_kana)が必須入力項目。
- それぞれ40文字以内。
- 姓と名で許される文字の種類は、漢字、ひらがな、カタカナ。
- 姓フリガナと名フリガナはカタカナのみ。ただし、ひらがなでの入力も受け付けて、カタカナに自動変換する。
- いわゆる半角カナは全角カナに自動変換する。
今回は、第1の仕様のみを扱います。
Customer
モデルの作成
顧客データを扱うためのモデルクラス Customer
と、それに対応するデータベーステーブル customers
を作成しましょう。
$ rails g model customer
db/migrate/..._create_customers.rb
を次のように修正して、
class CreateCustomers < ActiveRecord::Migration def change create_table :customers do |t| t.string :family_name, null: false t.string :given_name, null: false t.string :family_name_kana, null: false t.string :given_name_kana, null: false t.timestamps end end end
テスト環境のためのデータベースを準備します。
$ rake db:migrate $ rake db:test:prepare
not_to
メソッドと述語マッチャ
では、Customer
モデルの第1の仕様「姓、名、姓フリガナ、名フリガナが必須入力項目」を RSpec で記述し、テストしてみましょう。
rails g model customer
コマンドを実行した際に、副作用として spec/models/customer_spec.rb
というファイルが生成されます。初期状態では次のような内容です:
require 'spec_helper' describe Customer do pending "add some examples to (or delete) #{__FILE__}" end
まずは「姓(family_name)」について仕様を書いてみましょう。次のように修正してください。
require 'spec_helper' describe Customer do specify "family_name は空であってはならない" do customer = Customer.new( family_name: '', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) expect(customer).not_to be_valid expect(customer.errors[:family_name]).to be_present end end
10行目の expect(customer).not_to be_valid
は、「customer
が妥当(valid)ではないことを期待する」という意味です。このように、ある条件が成立しないことをテストするには、to
の代わりに not_to
を使用します。be_valid
は、テストの対象に対して valid?
メソッドを呼んで、それが返す値(真または偽)によってテストの成否を判定するマッチャです。valid?
は Rails の ActiveRecord が提供するメソッドで、モデルオブジェクトのバリデーションが成功したかどうか真偽値で返します。
11行目の be_present
も be_valid
と同様に、present?
メソッドが真を返せばテストを通し、偽を返せばテストを失敗させます。customer.errors[:family_name]
は family_name
カラムに関するエラーメッセージの配列を返します。配列に1個でも要素があれば present?
メソッドが真を返しテストが成功する、という仕組みです。
一般に、
be_xxx
という形式のマッチャは、特別にマッチャが定義されていないかぎり、xxx?
という名前のメソッドを呼んでテストの成否を判定します。この主のマッチャを、特に述語マッチャ(predicate matcher)と呼びます。
テストを通す
テストを実行します。
$ bin/rspec spec/models/customer_spec.rb
もちろん、テストは失敗します:
Failures: 1) Customer バリデーション family_name は空であってはならない Failure/Error: expect(customer).not_to be_valid expected #<Customer id: nil, family_name: "", given_name: "太郎", ...> not to be valid # ./spec/models/customer_spec.rb:10:in `block (2 levels) in <top (required)>'
app/models/customer.rb
にバリデーションを追加します。
class Customer < ActiveRecord::Base validates :family_name, presence: true end
これでテストは通ります。
specify
メソッドの繰り返し
続いて、その他の3つのカラムについても仕様を追加します。
require 'spec_helper' describe Customer do specify "family_name は空であってはならない" do # (省略) end specify "given_name は空であってはならない" do customer = Customer.new( family_name: '山田', given_name: '', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) expect(customer).not_to be_valid expect(customer.errors[:given_name]).to be_present end specify "family_name_kana は空であってはならない" do # (省略) end specify "given_name_kana は空であってはならない" do # (省略) end end
しかし、このように単純に繰り返すのは冗長です。一工夫してみましょう。
require 'spec_helper' describe Customer do %w{family_name given_name family_name_kana given_name_kana}.each do |column_name| specify "#{column_name} は空であってはならない" do customer = Customer.new( family_name: '山田', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) customer[column_name] = '' expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end end end
app/models/customer.rb
にバリデーションを追加すれば、テストが通ります。
class Customer < ActiveRecord::Base validates :family_name, :given_name, :family_name_kana, :given_name_kana, presence: true end
バリデーションのテストは必要か
ところで、そもそもこのような単純なバリデーションに対するテストは必要なのでしょうか。多くの場合、validates
メソッドの使い方はまったく難しくありません。モデルクラスのソースコードを目視で確認すればそれで十分なのではないでしょうか。
私自身はこれほど単純なバリデーションについては原則としてテストを書きません。第3の仕様「姓と名で許される文字の種類は、漢字、ひらがな、カタカナ」は少し複雑なのでテストを書くかもしれません。第4と第5の仕様については、必ずテストを書きます。
どこまでテストを書くべきか、は難しい問題です。別の機会に論じたいと思います。今回の例は、あくまで RSpec の書き方の例であると考えてください。
次回は
次回は、let
メソッドと Factory Girl について解説します。では、また。