背景
chisato-moritaka-db の時には、日本語・英語の振り分けを Mix-in の形で登録していました。今回は、さまざまなタイトルを個々のテーブルに持つのではなく、Title モデルを導入することにしました。タイトル一覧のページを作ることで、同一タイトルのオブジェクトの作成を簡単にするだけでなく、索引としての役割をもたせるためです。
Title モデルは japanese, english, yomi, yomi_suuji の 4 つの文字列属性を持ちます。yomi_suuji は読みを数値化したもので、データベース上で辞書順ソートができるようにしたものです。姓名をそれぞれ小数点表示をし、間を「-」でつなぎます。それぞれの整数部は濁点なしの文字に変換後に対応するコードに置き換えたもので設定し、小数部は濁点、半濁点のフラグで設定します。例えば「こばやし」であれば、「14507021.0100」となり、「こはら」であれば「145080.000」となります。文字で比較すると、「こばやし」が後ろになってしまいますが、変換した数値では正しく前になることがわかります。姓名の間を数字よりもアスキーコードが若い「-」に設定しているので、「こば」は「こばやし」よりも前に並びます。なお、この yomi_suuji は yomi 属性から自動的に生成されるものとします。
データベーステーブルの設定
まず、migration ファイルを generator で作成します。
r g model title japanese:string english:string yomi:string yomi_suuji:string
以下のファイルが作成されます。factory_girl の spec も生成されています。
invoke active_record create db/migrate/20140202232237_create_titles.rb create app/models/title.rb invoke rspec create spec/models/title_spec.rb invoke factory_girl create spec/factories/titles.rb
上記の属性はすべて not null を期待しているので、migration に null:false を設定しておきます。
class CreateTitles < ActiveRecord::Migration def change create_table :titles do |t| t.string :japanese, null:false t.string :english, null:false t.string :yomi, null:false t.string :yomi_suuji, null:false t.timestamps end end end
データベースを migration しておきます。テストデータも準備しておきます。
be rake db:migrate
be rake db:test:prepare
FactoryGirl の設定
Factory Girl を使う前に関連設定をしておきます。gaurd init しただけでは、FactoryGirl ファイルの watch 設定がされていないので、FactoryGirlのファイルが変更されたタイミングでテストを自動的に実行するを参考に Guard ファイルに以下の設定を行います。
# FactoryGirl watch(%r{^spec/factories/(.+)\.rb$}) { |m| [ "spec/models/#{m[1].singularize}.rb", "spec/controllers/#{m[1]}_controller_spec.rb" ] }
ここで、単数形を求めるために singularize を使用しているため、Guardfile の先頭で inflector.rb を require しておきます。
require 'active_support/inflector'
また、FactoryGirl 更新時に変更が適用されるように、config/application.rb に FactoryGirl のリロードを追加しておきます。
# FactoryGirl config.before(:all) do FactoryGirl.reload end
テスト及び実装のイテレーション
まず、spec/factories/titles.rb を修正します。
FactoryGirl.define do factory :title do japanese "日本語 英語" english "English Japanese" yomi "にほんご えいご" yomi_suuji "ダミーデータ" end end
この設定の結果、FactoryGirl.build(:title) とすることで、必要な属性を持った Title オブジェクトが作成されます。これを使って spec を書いていきます。まず、FactoryGirl でオブジェクトが作成できることを確認します。
describe Title do let(:title) { FactoryGirl.build(:title) } it '妥当なオブジェクトが生成されること' do expect(title).to be_valid end end
ここで let は title という特殊なメソッドを定義しているものです。初回はオブジェクトを作成しますが、二回目以降は最初に生成したオブジェクトを返します。以前は以下のようなメソッドを自分で作成していましたが、それが簡単にかけるようになった感じでしょうか。
def title; @title ||= FactoryGirl.build(:title); end
取り急ぎ Guard の結果は以下のようになり、テストが通過しています。
Title 妥当なオブジェクトが生成されること Finished in 0.01572 seconds 1 example, 0 failures
しかしながら、データベースのテーブルには not null 制約を付けているので、Rails 側でも validation をする必要があります。そのため、yomi_suuji を除いた 3 つの属性が空欄の時にエラーとなるかどうかを確認します。
[ :japanese, :english, :yomi ].each do |key| it "#{key} は空であってはならない" do title[key] = "" expect(title).not_to be_valid expect(title.errors[key]).to be_present end end
Guard の結果は以下のようになります。本来エラーになるべきのものが、成功しているためにテストとしてはエラーとなります。
Failures: 1) Title english は空であってはならない Failure/Error: expect(title).not_to be_valid expected #<Title id: nil, japanese: "日本語 英語", english: "", yomi: "にほんご えいご", yomi_suuji: "ダミーデータ, created_at: nil, updated_at: nil> not to be valid # ./spec/models/title_spec.rb:13:in `block (3 levels) in <top (required)>' # -e:1:in `<main>' 2) Title japanese は空であってはならない Failure/Error: expect(title).not_to be_valid expected #<Title id: nil, japanese: "", english: "English Japanese", yomi: "にほんご えいご", yomi_suuji: "ダミーデータ", created_at: nil, updated_at: nil> not to be valid # ./spec/models/title_spec.rb:13:in `block (3 levels) in <top (required)>' # -e:1:in `<main>' 3) Title yomi は空であってはならない Failure/Error: expect(title).not_to be_valid expected #<Title id: nil, japanese: "日本語 英語", english: "English Japanese", yomi: "", yomi_suuji: "ダミーデータ", created_at: nil, updated_at: nil> not to be valid # ./spec/models/title_spec.rb:13:in `block (3 levels) in <top (required)>' # -e:1:in `<main>' Finished in 0.02257 seconds 4 examples, 3 failures Failed examples: rspec ./spec/models/title_spec.rb:11 # Title english は空であってはならない rspec ./spec/models/title_spec.rb:11 # Title japanese は空であってはならない rspec ./spec/models/title_spec.rb:11 # Title yomi は空であってはならない
テストを通過させるために、app/models/title.rb に validation を追加します。
class Title < ActiveRecord::Base validates :japanese, :english, :yomi, :yomi_suuji, presence:true end
Guard の結果は以下のようになります。テストは全て成功しています。
Title 妥当なオブジェクトが生成されること english は空であってはならない japanese は空であってはならない yomi は空であってはならない Finished in 0.04914 seconds 4 examples, 0 failures
yomi_suuji の自動生成
現在、yomi_suuji にはダミーデータを入れています。本来、yomi_suuji は自動設定されるべきものなので、FactoryGirl のダミーデータを削除して "" だけにしてみます。
FactoryGirl.define do factory :title do japanese "日本語 英語" english "English Japanese" yomi "にほんご えいご" yomi_suuji "" end end
Guard の結果は以下のようになります。yomi_suuji が空のためにエラーになってしまいました。
Failures: 1) Title 妥当なオブジェクトが生成されること Failure/Error: expect(title).to be_valid expected #<Title id: nil, japanese: "日本語 英語", english: "English Japanese", yomi: "にほんご えいご", yomi_suuji: "", created_at: nil, updated_at: nil> to be valid, but got errors: Yomi suujiを入力してください。 # ./spec/models/title_spec.rb:7:in `block (2 levels) in <top (required)>' # -e:1:in `<main>' Finished in 0.05041 seconds 4 examples, 1 failure
yomi_suuji の validation でエラーとなっているので、yomi_suuji は validation 前に実行する必要があります。このため、title クラスに before_validation を設定します。ここでは yomi の文字列を空白で区切って、それぞれを yomi_suuji に変換した後、「-」で結合することで、仕様を満たす文字列が生成されることになります。
class Title < ActiveRecord::Base include YomiSuuji validates :japanese, :english, :yomi, :yomi_suuji, presence:true before_validation do self.yomi_suuji = self.yomi.split(' ').map { |str| convert_yomi_suuji(str) }.join('-') end end
肝心の読み数字を生成する convert_yomi_suuji ですが、これまでにもいろんなアプリで使っており、Module 化されているのでそれを利用します。以前は、lib などのフォルダに関連モジュールなどを配置していましたが、Rails4 の場合には concerns フォルダに配置するようになりました。今回は以下のような app/models/concerns/yomi_suuji.rb を記載します。以前は、1文字ずつのマッピングテーブルを用意していましたが、行と列のデータを後から zip すればよいことに気づき、tr だけで簡潔に記述するように変わっています。
module YomiSuuji Kana = [*"ぁ".."ゖ"].join("") + "ー・" def yomi_suuji_sub(str) first = (str.tr Kana, "0000000000111111111122222222223333333333344444555555555555555666667777778888899999951199").split(//) second = (str.tr Kana, "0011223344001122334400112233440011222334401234000111222333444012340022440123400134520399").split(//) last = str.tr Kana, "0000000000010101010101010101010101001010100000012012012012012000000000000000000000010000" [ first.zip(second).flatten.join(""), last].join('.') end end
Guard の結果は以下のようになります。yomi_suuji が設定されて、「妥当なオブジェクトが生成されること」が成功になっています。
Title 妥当なオブジェクトが生成されること yomi は空であってはならない japanese は空であってはならない english は空であってはならない Finished in 0.04692 seconds 4 examples, 0 failures
正しく、yomi_suuji が入っているかが心配なので、yomi_suuji の文字列もテストします。validation が発生しないと yomi_suuji が設定されないので、validation check を行っています。
it "yomi_suuji が正しく設定できること" do title.valid? expect(title.yomi_suuji).to eq('41549514.0001-030114.001') end
Guard の結果はこうなりました。
Title 妥当なオブジェクトが生成されること japanese は空であってはならない yomi は空であってはならない english は空であってはならない yomi_suuji が正しく設定できること Finished in 0.05624 seconds 5 examples, 0 failures
せっかくなので、全文字が正しく変換できているかを確認してみます。
title.yomi = [*"ぁ".."ゖ"].join(" ") + " ー・" title.valid? expect(title.yomi_suuji).to eq('00.0-00.0-01.0-01.0-02.0-02.0-03.0-03.0-04.0-04.0-10.0-10.1-11.0-11.1-12.0-12.1-13.0-13.1-14.0-14.1-20.0-20.1-21.0-21.1-22.0-22.1-23.0-23.1-24.0-24.1-30.0-30.1-31.0-31.1-32.0-32.0-32.1-33.0-33.1-34.0-34.1-40.0-41.0-42.0-43.0-44.0-50.0-50.1-50.2-51.0-51.1-51.2-52.0-52.1-52.2-53.0-53.1-53.2-54.0-54.1-54.2-60.0-61.0-62.0-63.0-64.0-70.0-70.0-72.0-72.0-74.0-74.0-80.0-81.0-82.0-83.0-84.0-90.0-90.0-91.0-93.0-94.0-95.0-52.1-10.0-13.0-9999.00')
すべての文字が変換できていることが確認できました。
読みの検査
読みに正しくひらがなのみが設定されていれば問題がないのですが、そうでない場合には、読み数字に変な文字列が設定されてしまいます。設定された文字列以外のものが読みに設定された時にエラーが出るように設定します。その確認のためにテストを追加します。
it "読みにひらがなと設定された記号以外の文字が設定されていた場合にエラーとなること" do title.yomi = "日本語" expect(title).not_to be_valid expect(title.errors[:yomi]).to be_present end
Guard の結果は以下のようになります。
Failures: 1) Title 読みにひらがなと設定された記号以外の文字が設定されていた場合にエラーとなること Failure/Error: expect(title).not_to be_valid expected #<Title id: nil, japanese: "日本語 英語", english: "English Japanese", yomi: "日本語", yomi_suuji: "日日本本語語.日本語", created_at: nil, updated_at: nil> not to be valid # ./spec/models/title_spec.rb:28:in `block (2 levels) in <top (required)>' # -e:1:in `<main>' Finished in 0.07446 seconds 6 examples, 1 failure Failed examples:
yomi_suuji になかなか楽しい文字列が設定されていることがわかります。この場合にエラーになるように app/models/title.rb に validation を追加します。
validates :yomi, format: { with: /\A[\p{Hiragana}ー・ ]+\z/, allow_blank: true }
この結果、Guard では、6 つのテストが無事に成功したことになります。
Title 妥当なオブジェクトが生成されること yomi は空であってはならない english は空であってはならない japanese は空であってはならない yomi_suuji が正しく設定できること 読みにひらがなと設定された記号以外の文字が設定されていた場合にエラーとなること Finished in 0.06541 seconds 6 examples, 0 failures
とりあえず、Title モデルについては今ところはここまでとなります。
written by iHatenaSync