勉強会BDD偏＜２＞RSpecとCapybara - モデル


## 関連記事

* [勉強会BDD＜１＞RSpecとCapybaraースタート](http://qiita.com/park-jh/items/a6327a96e771857bc78c)
* 勉強会BDD＜２＞RSpecとCapybaraーモデル
* [勉強会BDD＜３＞RSpecとCapybaraーユーザー認証](http://qiita.com/park-jh/items/ed64de86ad9aaa6a3635)
* [勉強会BDD＜４＞RSpecとCapybaraーポイントシステム](http://qiita.com/park-jh/items/7726820d950f03f8b061)

## この記事について

* この記事はoiaxというサイトから提供された記事を元に社内で勉強会を行う目的で作成した。
* 詳細な内容が知りたい場合はoiaxの記事を参考にしていただきたい。
* oiaxの記事と大きな違いは社内の開発ツールとしてvimを使っているので、vim環境ですぐスペックの確認ができるようにしている。
* そして、詳細説明は省略して実習モードで構成されている。

## BDDで開発するためには

1. ある機能のスペック（仕様）を先に洗い出す。
2. そのスペックを元にテストコードを作成する。
3. テストコードを書いたら、とりあえず実行して失敗になることを確認する。
4. その後、機能を実装する。
5. テストが成功するまで2~4を繰り返す。

## モデルのテスト

* http://www.oiax.jp/rails/rspec_capybara_primer/model_test.html

### Customerモデルの仕様

顧客モデルの仕様を先に洗い出す。

* 姓（family_name）、名（given_name）、姓フリガナ（family_name_kana）、名フリガナ（given_name_kana）が必須入力項目
* それぞれ40文字以内。
* 姓と名で許される文字の種類は、漢字、ひらがな、カタカナ。
* 姓フリガナと名フリガナはカタカナのみ。ただし、ひらがなでの入力も受け付けて、カタカナに自動交換する。
* いわゆる半角は全角カナに自動交換する。

### Customerモデルの作成

railsのgeneratorを使って雛形を生成する。

```ruby
# generate model
bin/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 null: false
    end
  end
end

# migration
bin/rake db:migrate
bin/rake db:test:prepare

# spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  example '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

# ここまでしておいてテスト
# , + c

# app/models/customer.rb
class Customer < ActiveRecord::Base
  validates :family_name, presence: true
end

# spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  %w{family_name given_name family_name_kana given_name_kana}.each do |column_name|
    example "#{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, presence: true
  validates :given_name, presence: true
  validates :family_name_kana, presence: true
  validates :given_name_kana, presence: true
end

# もう一度テスト
# , + c
```

## letメソッドとFactory Girl

* テストコードの簡素化
* http://www.oiax.jp/rails/rspec_capybara_primer/let_and_factory_girl.html

### 妥当なオブジェクトのテストを追加する。

```ruby
# spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  example '妥当なオブジェクト' do
    customer = Customer.new(
      family_name: '山田', given_name: '太郎',
      family_name_kana: 'ヤマダ', given_name_kana: 'タロウ'
    )
    expect(customer).to be_valid
  end

  %w{family_name given_name family_name_kana given_name_kana}.each do |column_name|
    example "#{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
```

### let methodを利用して重複を解消する。

* letはメモかされたヘルパーメソッドを定義するメソッド。
* letで定義されたメソッドは各exampleごとに読み出される。（各exampleごとにインスタンスを作り直す。)

```ruby
# spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  let(:customer) do
    Customer.new(
      family_name: '山田', given_name: '太郎',
      family_name_kana: 'ヤマダ', given_name_kana: 'タロウ'
    )
  end

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

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

    example "#{column_name}は40文字以内" do
      customer[column_name] = 'ア' * 41
      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, presence: true, length: {maximum: 40}
  validates :given_name, presence: true, length: {maximum: 40}
  validates :family_name_kana, presence: true, length: {maximum: 40}
  validates :given_name_kana, presence: true, length: {maximum: 40}
end
```

### Factory Girlを利用する。

* テストをするたびにCustomer.newやCustomer.createに属性と値のハッシュを与えて、妥当なCustomerオブジェクトを作るは冗長。
* これを解消するためにファクトリーを定義して使う。

```ruby
# spec/factories/customers.rb
FactoryGirl.define do
  factory :customer do
    family_name '山田'
    given_name '太郎'
    family_name_kana 'ヤマダ'
    given_name_kana 'タロウ'
  end
end

# spec/models/customer.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  let(:customer) { FactoryGirl.build(:customer) }

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

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

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

## モデルテストの演習

* http://www.oiax.jp/rails/rspec_capybara_primer/model_test_exercise.html

### 文字の種類を制限する。

```ruby
# spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  let(:customer) { FactoryGirl.build(:customer) }

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

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

    example "#{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|
    example "#{column_name}は漢字、ひらがな、カタカナを含んでもよい" do
      customer[column_name] = '亜あア'
      expect(customer).to be_valid
    end

    example "#{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

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

### 正規表現マッチ

```
\A: 文字列の先頭
\z: 文字列の末尾
\p{Han}: 任意の漢字
\p{Hiragana}: 任意のひらがな
\p{Katakana}: 任意のカタカナ
```

### eqマッチャ

* 未実装の仕様
  + 性と名で許される文字の種類は、漢字、ひらがな、カタカナ。
  + 姓フリガナ名フリガナはカタカナのみ。ただし、ひらがなでの入力も受け付けて、カタカナに自動交換する。
  + いわゆる半角カナは全角カナに自動交換する。
* テストケースを追加

```ruby
# spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  let(:customer) { FactoryGirl.build(:customer) }

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

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

    example "#{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|
    example "#{column_name}は漢字、ひらがな、カタカナを含んでもよい" do
      customer[column_name] = '亜あア'
      expect(customer).to be_valid
    end

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

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

end

# app/models/customer.rb
require 'nkf'

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

  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
```

### 補遺

* \p{Hiragana}及び\p{Katakana}には長音符が含まれていない。
* テストケースに長音符を入れるとエラーになるので、修正する。

```diff
diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb
index 0d24227..9c35634 100644
--- a/spec/models/customer_spec.rb
+++ b/spec/models/customer_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Customer, type: :model do

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

diff --git a/app/models/customer.rb b/app/models/customer.rb
index ee92520..f83df81 100644
--- a/app/models/customer.rb
+++ b/app/models/customer.rb
@@ -2,9 +2,9 @@ require 'nkf'

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

### 参考

* http://railsguides.jp/active_record_validations.html