モデルのテスト

2013/08/18

今回は、Ruby on Rails のモデルクラスに対するテストの書き方を解説します。

Customer モデルの仕様

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_presentbe_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 について解説します。では、また。