モデルテストの演習

2013/08/25

ru

前回のletメソッドとFactory Girlでは、テスト対象のオブジェクトをエグザンプルのコードから外に出すことで、テストを簡素化する方法を学びました。

今回は、モデルテストの演習です。様々な仕様を RSpec のエグザンプルとして記述し、実装し、テストを通します。

Customer モデルの実装されていない仕様

以前Customer モデルの主な仕様として挙げたもののうち、未実装なのは以下の3つです。

  • 姓と名で許される文字の種類は、漢字、ひらがな、カタカナ。
  • 姓フリガナと名フリガナはカタカナのみ。ただし、ひらがなでの入力も受け付けて、カタカナに自動変換する。
  • いわゆる半角カナは全角カナに自動変換する。

2番目の仕様は1行で書いてありますが、実質的には2つの仕様です。つまり、全部で4つです。これらをひとつずつ片付けていきましょう。

文字の種類を制限する

まず「姓と名で許される文字の種類は、漢字、ひらがな、カタカナ」です。これを RSpec で表現すれば、次のようになります(24-37行が追加箇所)。

require 'spec_helper'

describe Customer do
  let(:customer) { FactoryGirl.build(:customer) }

  specify '妥当なオブジェクト' do
    expect(customer).to be_valid
  end

  %w{family_name given_name family_name_kana given_name_kana}.each do |column_name|
    specify "#{column_name} は空であってはならない" do
      customer[column_name] = ''
      expect(customer).not_to be_valid
      expect(customer.errors[column_name]).to be_present
    end

    specify "#{column_name} は40文字以内" do
      customer[column_name] = 'ア' * 41
      expect(customer).not_to be_valid
      expect(customer.errors[column_name]).to be_present
    end
  end

  %w{family_name given_name}.each do |column_name|
    specify "#{column_name} は漢字、ひらなが、カタカナを含んでもよい" do
      customer[column_name] = '亜あア'
      expect(customer).to be_valid
    end

    specify "#{column_name} は漢字、ひらなが、カタカナ以外の文字を含まない" do
      ['A', '1', '@'].each do |value|
        customer[column_name] = value
        expect(customer).not_to be_valid
        expect(customer.errors[column_name]).to be_present
      end
    end
  end
end

不正な文字列の例として「A」と「1」と「@」を指定しています。他にも句読点とかギリシャ文字などバリデーションで禁じたい文字種は存在しますが、きりがありませんので、このぐらいでいいでしょう。

さて、「漢字、ひらなが、カタカナ以外の文字を含まない」という制約条件を Active Model のバリデーションでどう表現すればよいでしょうか。正規表現を使うのでは、というところまでは思い当たるかもしれませんが、その正規表現を本を読んだり、ネットで検索したりせずにそらで書ける人はあまりいません。筆者自身も書けません。多くの方は正解を得るまでに試行錯誤を繰り返すことになるでしょう。

テスト駆動開発の効果が発揮されるのは、こういう場合です。テストが存在しなければ、モデルのコードを変更しながら様々な値をオブジェクトの属性にセットして目視で結果を確かめる、という行為を繰り返さなければなりません。テストが存在すれば、モデルのコードが正しいかどうかコマンド一発で確かめられます。

結論だけ言えば、この仕様が実装された後の app/models/customer.rb のコードは次のようになります:

class Customer < ActiveRecord::Base
  validates :family_name, :given_name, :family_name_kana, :given_name_kana,
    presence: true, length: { maximum: 40 }
  validates :family_name, :given_name,
    format: { with: /\A[\p{Han}\p{Hiragana}\p{Katakana}]+\z/, allow_blank: true }
end

\A は「文字列の先頭」、\zは「文字列の末尾」にマッチします。\p{Han} は「任意の漢字」、\p{Hiragana} は「任意のひらがな」、\p{Katakana} は「任意のカタカナ」にマッチします。

この文章を発表した後で、バリデーションコードの不備に気付きました。ページ末尾の「補遺」をご覧ください。

なお、allow_blank: true オプションを指定しているのは、presence: true によるチェックと重複しないようにするためです。このオプションを指定しないと、姓または名に空文字(空白のみから成る文字列を含む)が指定された場合に、バリデーションエラーが2個登録されてしまいます。

ここまでの知識を使うと、2番目の仕様の前半部分「姓フリガナと名フリガナはカタカナのみ」のテストおよびバリデーションの実装を行うことができます。これは、読者の皆さんへの宿題としましょう(答えは次回発表)。

eq マッチャ

次に、2番目の仕様の後半部分「(姓フリガナと名フリガナは)ひらがなでの入力も受け付けて、カタカナに自動変換する」に進みましょう。RSpec のコードは次のようになります:

require 'spec_helper'

describe Customer do
  let(:customer) { FactoryGirl.build(:customer) }

  specify '妥当なオブジェクト' do
    expect(customer).to be_valid
  end

  # (省略)

  %w{family_name_kana given_name_kana}.each do |column_name|
    specify "#{column_name} に含まれるひらがなはカタカナに変換して受け入れる" do
      customer[column_name] = 'あいう'
      expect(customer).to be_valid
      expect(customer[column_name]).to eq('アイウ')
    end
  end
end

下から4行目に新しいマッチャ eq が登場しています。引数に指定したオブジェクトと、テスト対象のオブジェクトが等しいかどうかを確かてくれます。なお、RSpec には類似のマッチャ eqlequal があります。eq では、テスト対象のオブジェクトの == メソッドを用いて等価性が確認されますが、eql を使用した場合は eql? メソッドで、equal を使用した場合は equal? メソッドで判断されます。これらのメソッドの違いは、http://d.hatena.ne.jp/k-sato_at_oiax/20100614/1276519946 を参照してください。文字列や数値といった「普通のオブジェクト」同士が等しいかどうか確認するには、eq マッチャを使います。

app/models/customer.rb のコードは次のようになります(変更箇所は、1,9-12行):

require 'nkf'

class Customer < ActiveRecord::Base
  validates :family_name, :given_name, :family_name_kana, :given_name_kana,
    presence: true, length: { maximum: 40 }
  validates :family_name, :given_name,
    format: { with: /\A[\p{Han}\p{Hiragana}\p{Katakana}]+\z/, allow_blank: true }

  before_validation do
    self.family_name_kana = NKF.nkf('-wh2', family_name_kana) if family_name_kana
    self.given_name_kana = NKF.nkf('-wh2', given_name_kana) if given_name_kana
  end
end

NKF は Ruby 標準ライブラリに含まれる漢字コード変換モジュールです。nkf の第一引数には、同名の nkf プログラムに与えるオプションを指定し、第二引数には変換対象の文字列を指定します。ここで使用しているオプションのうち w は「UTF-8で出力する」という意味で、h2が「ひらがなをカタカナに変換する」という意味になります。

最後に残った仕様「いわゆる半角カナは全角カナに自動変換する」については、ここまでの知識を総動員すればテストを書いて、テストを通すことができるはずです。これも読者の皆さんへの宿題としましょう。ネットでNKFのドキュメントを探して調べてください。なお、NKFのドキュメントは「いわゆる半角カナ」のことを「X0201 kana」と呼んでいます。

次回は

次回は、ユーザー認証のテストを扱います。少し大きなテーマなので、1回では終わらないでしょう。では、また。

補遺(2013/10/14)

この文章を発表した後、私は顧客との会話の中で、バリデーションコードの不備に気付きました。実は、正規表現で使用する \p{Hiragana} および \p{Katakana} には長音符(音引き)が含まれていません。つまり、「アーロン」という名前は不正な値と判定されてしまうのです。

このことは、テストコードの

    specify "#{column_name} は漢字、ひらなが、カタカナを含んでもよい" do
      customer[column_name] = '亜あア'
      expect(customer).to be_valid
    end

とある部分を

    specify "#{column_name} は漢字、ひらなが、カタカナを含んでもよい" do
      customer[column_name] = '亜あアーン'
      expect(customer).to be_valid
    end

と書き換えてテストを実行すればすぐに分かります。そして、長音符(U+30FC)を正規表現の中に加えればテストは通ります:

require 'nkf'

class Customer < ActiveRecord::Base
  validates :family_name, :given_name, :family_name_kana, :given_name_kana,
    presence: true, length: { maximum: 40 }
  validates :family_name, :given_name,
    format: { with: /\A[\p{Han}\p{Hiragana}\p{Katakana}\u30fc]+\z/, allow_blank: true }

  before_validation do
    self.family_name_kana = NKF.nkf('-wh2', family_name_kana) if family_name_kana
    self.given_name_kana = NKF.nkf('-wh2', given_name_kana) if given_name_kana
  end
end

\u30fc と書く代わりに と書いても構わないのですが、マイナス記号や罫線と紛らわしいので 16 進数の Unicode 番号で記述してあります。

以上、訂正させていただきます。

[更新] 長音符を含む名前のバリデーションに関する補遺を追加しました。(2013/10/14)