単体テストの応用〜factory_bot〜
factory_bot
簡単にダミーのインスタンスを作成することができるGem。他のファイルで予め各クラスのインスタンスに定めるプロパティを設定しておき、specファイルからメソッドを利用してその通りのインスタンスを作成する。factory_botを利用すれば、user_spec.rbは以下のように短い記述にすることができる。
【例】factory_botを利用した場合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
require 'rails_helper'
describe User do
describe '#create' do
it "is invalid without a nickname" do
user = build(:user, nickname: nil)
user.valid?
expect(user.errors[:nickname]).to include("can't be blank")
end
it "is invalid without a email" do
user = build(:user, email: nil)
user.valid?
expect(user.errors[:email]).to include("can't be blank")
end
end
end
|
Userクラスのインスタンスを作成している部分が随分短く記述されていることがわかる。では早速、factory_botを利用してみる。
以下の指示に従ってGemfileを編集
rspecと同じグループの中に追記する。
その後、bundle install
を行う。
続いて、specディレクトリ直下に「factories」というディレクトリを作成。その中に、作成したインスタンスの複数形のファイル名でRubyのファイルを作成する。今回の場合は、users.rb
である。
specディレクトリ直下に「factories」というディレクトリを追加し、その中に「users.rb」という名前でファイルを作成。
- pictweet
- app 省略・・・
- spec
- models
- specファイル
- controllers
- specファイル
- factories
- users.rb
- models
続いてusers.rbを編集する。
1 2 3 4 5 6 7 8 9 10 |
これで準備は完了。このようにすると、specファイルの中で特定のメソッドにより簡単にインスタンスを生成したり、DBに保存したりできるようになる。
続いて、factory_botにおける最も基本的なメソッドであるbuild
メソッドとcreate
メソッドについて。
buildメソッド
引数にシンボル型で取ったクラス名のインスタンスを、factory_botの記述をもとに作成する。例えば前述のusers.rbが存在する場合、下記2つの変数userの値は同じ値になる。
1 2 3 4 |
createメソッド
buildとほぼ同じ働きをするが、createの場合はテスト用のDBに値が保存される。
注意すべき点として、1回のテストが実行され、終了する毎にテスト用のDBの内容がロールバックされる。(保存された値がすべて消去されてしまう)
従って、binding.pry等でテストの実行を一時停止しないとテスト用のDBに保存された値をSequel Pro等で確認することはできない。
factory_botの記法の省略
factory_botによってインスタンスを作成する際に、レシーバーであるクラスのFactoryBot
という記述を省略することができる。そのためには、spec/rails_helper.rbを以下のように編集する。
1 2 3 4 5 6 7 8 |
#省略
RSpec.configure do |config|
#下記の記述を追加
config.include FactoryBot::Syntax::Methods
#省略
end
|
すると、先ほどのuser_spec.rbの記述はFactoryBot
を省いて以下のように省略できる。
1 2 3 4 5 6 |
#nicknameが空では登録できないこと
it "is invalid without a nickname" do
user = build(:user, nickname: "")
user.valid?
expect(user.errors[:nickname]).to include("can't be blank")
end
|
セットした値の上書き
factoriesディレクトリ内のファイルで予めセットした値を変更しインスタンスを生成したい場合は、specファイル側で引数を増やし、上書きすることができる。
引数はカラム名: 値
という形のハッシュで、いくつでも追加することができる。
1 2 3 4 5 6 7 8 9 10 11 |
Userクラスのインスタンスをfactory_botを利用した生成に書き直す
問題1:user_spec.rbの中で生成しているUserクラスのインスタンスをfactory_botを利用した生成に書き直す。
作業ファイル:app/spec/models/user_spec.rb
ヒント:Userクラスを利用して作成している部分を、factory_botを利用したものに書き換える
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
require 'rails_helper'
describe User do
describe '#create' do
it "is invalid without a nickname" do
user = build(:user, nickname: "")
user.valid?
expect(user.errors[:nickname]).to include("can't be blank")
end
it "is invalid without an email" do
user = build(:user, email: "")
user.valid?
expect(user.errors[:email]).to include("can't be blank")
end
end
end
|
解説
これまでUser.new()としていた部分をbuild(:user)とすると、同様にUserクラスのインスタンスを作成できるようになっている。
buildメソッドの引数にはシンボル型で作成したいクラスの名前を渡すが、第二引数以降カラム名: 値という形式でハッシュを渡すことで、生成されるインスタンスのプロパティ値を上書きすることができる。
テストコードを書く際の原則について
テストコードを書く際の原則
テストコードを書くにあたっては、守るべき以下の原則がある。これらはとても大切なので、必ず覚えておく。
①各exampleで期待する値は1つ
②期待する結果をはっきりわかりやすく記述
③起きて欲しいことと起きてほしくないこと両方をテストする
④境界値をテストする
⑤可読性を考えつつ、適度にDRYにする
①各exampleで期待する値は1つ
テストコードにおいては、example(it "hoge" do ~ end のまとまり)ひとつに必ずエクスペクテーション(expext(◯◯).to ~)をひとつ含める。
2つ以上含めてしまうと、どちらのエクスペクテーションでエラーが出たのか判別できず、正確なテストができないため。
②期待する結果をはっきりわかりやすく記述する
it "〜" do
の"〜"
の部分は、期待する結果を書いておく場所。
明快な書き方をすることで、自身の確認やチームメンバーとの共有、顧客への仕様説明が楽になり、コミュニケーションミスも減る。
③起きて欲しいことと起きてほしくないこと両方をテストする
起きて欲しいことをチェックしたら、起きてほしくない場合にどんな結果が起こるかも想定しその通りになるか確かめる。
予期せぬ動作が残るのを防ぐため。
④境界値をテストする
6文字以上でバリデーションに引っかかる、という条件の場合は「5文字までは正常」と「6文字以上ならば異常」を確かめる。こちらも、予期せぬ動作を防ぐため。
⑤可読性を考えつつ、適度にDRYにする
DRYとは「Don't Repeat Yourself」の略で、何度も同じことを記述せず効率的にコードを書こう、という原則を意味する。しかし、テストコードにおいては何よりもわかりやすさを優先する。その結果、たとえDRYに添えなくなったとしても、わかりづらくなってテストの見落としが起きるよりはましだからである。
全ての条件を網羅したバリデーションテストを書く
では、前述したテストコードの原則に沿って、Pictweetのユーザー新規登録時のバリデーションに関するテストコードを完成させる。
テストすべき項目一覧
今回、ユーザーの登録時に確認が必要なバリデーションのテストは以下。
- nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
- nicknameが空では登録できないこと
- emailが空では登録できないこと
- passwordが空では登録できないこと
- passwordが存在してもpassword_confirmationが空では登録できないこと
- nicknameが7文字以上であれば登録できないこと
- nicknameが6文字以下では登録できること
- 重複したemailが存在する場合登録できないこと
- passwordが6文字以上であれば登録できること
- passwordが5文字以下であれば登録できないこと
このうち、いくつかのものは少し工夫が必要である。
nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
まずは全ての条件が整っている場合に「登録ができること」を確かめるパターン。この場合は、「正常に保存されることを期待する」エクスペクテーションが必要。この時利用するマッチャがbe_valid
マッチャである。
be_validマッチャ
expectの引数にしたインスタンスが全てのバリデーションをクリアする場合にパスするマッチャである。
be_validマッチャを利用すると、この条件のexampleは以下のように簡単に書くことができる。
1 2 3 4 |
it "is valid with a nickname, email, password, password_confirmation" do
user = build(:user)
expect(user).to be_valid
end
|
重複したemailが存在する場合登録できないこと
続いて、重複に関するパターン。先にユーザーを1人登録しておき、その後emailに同じ値を持つ別のユーザーが登録できるかチェックすることで確かめられる。
この時含まれるエラー文は、「has already been taken」。
1 2 3 4 5 6 7 8 |
it "is invalid with a duplicate email address" do
#はじめにユーザーを登録
user = create(:user)
#先に登録したユーザーと同じemailの値を持つユーザーのインスタンスを作成
another_user = build(:user)
another_user.valid?
expect(another_user.errors[:email]).to include("has already been taken")
end
|
では、これらも含めた全てのバリデーションに関するテストコードを書いてみる。
Pictweetの新規ユーザー登録時のテストコードを完成させる
今回、ユーザーの登録時に確認が必要なバリデーションのテストは以下。
テストすべき項目一覧
- nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
- nicknameが空では登録できないこと
- emailが空では登録できないこと
- passwordが空では登録できないこと
- passwordが存在してもpassword_confirmationが空では登録できないこと
- nicknameが7文字以上であれば登録できないこと
- nicknameが6文字以下では登録できること
- 重複したemailが存在する場合登録できないこと
- passwordが6文字以上であれば登録できること
- passwordが5文字以下であれば登録できないこと
問題2:Pictweetの新規ユーザー登録時のバリデーションに関するテストコードを完成させる。
作業ファイル:app/spec/models/user_spec.rb
ヒント1:テストすべき項目一覧を確認しつつ全てのテストコードを書いていく
ヒント2:「何文字以内」もしくは「何文字以上」という条件のバリデーションでどのようなエラー文が出るか、binding.pryを利用して試してみる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
require 'rails_helper'
describe User do
describe '#create' do
# 1. nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
it "is valid with a nickname, email, password, password_confirmation" do
user = build(:user)
expect(user).to be_valid
end
# 2. nicknameが空では登録できないこと
it "is invalid without a nickname" do
user = build(:user, nickname: nil)
user.valid?
expect(user.errors[:nickname]).to include("can't be blank")
end
# 3. emailが空では登録できないこと
it "is invalid without a email" do
user = build(:user, email: nil)
user.valid?
expect(user.errors[:email]).to include("can't be blank")
end
# 4. passwordが空では登録できないこと
it "is invalid without a password" do
user = build(:user, password: nil)
user.valid?
expect(user.errors[:password]).to include("can't be blank")
end
# 5. passwordが存在してもpassword_confirmationが空では登録できないこと
it "is invalid without a password_confirmation although with a password" do
user = build(:user, password_confirmation: "")
user.valid?
expect(user.errors[:password_confirmation]).to include("doesn't match Password")
end
# 6. nicknameが7文字以上であれば登録できないこと
it "is invalid with a nickname that has more than 7 characters " do
user = build(:user, nickname: "aaaaaaaa")
user.valid?
expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")
end
# 7. nicknameが6文字以下では登録できること
it "is valid with a nickname that has less than 6 characters " do
user = build(:user, nickname: "aaaaaa")
expect(user).to be_valid
end
# 8. 重複したemailが存在する場合登録できないこと
it "is invalid with a duplicate email address" do
user = create(:user)
another_user = build(:user, email: user.email)
another_user.valid?
expect(another_user.errors[:email]).to include("has already been taken")
end
# 9. passwordが6文字以上であれば登録できること
it "is valid with a password that has more than 6 characters " do
user = build(:user, password: "000000", password_confirmation: "000000")
user.valid?
expect(user).to be_valid
end
# 10. passwordが5文字以下であれば登録できないこと
it "is invalid with a password that has less than 5 characters " do
user = build(:user, password: "00000", password_confirmation: "00000")
user.valid?
expect(user.errors[:password]).to include("is too short (minimum is 6 characters)")
end
end
end
|
解説
改めて、今回テストすべき項目を列挙してみます。
- nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
- nicknameが空では登録できないこと
- emailが空では登録できないこと
- passwordが空では登録できないこと
- passwordが存在してもpassword_confirmationが空では登録できないこと
- nicknameが7文字以上であれば登録できないこと
- nicknameが6文字以下では登録できること
- 重複したemailが存在する場合登録できないこと
- passwordが6文字以上であれば登録できること
- passwordが5文字以下であれば登録できないこと
このうち、「1,2,3,8」に関しては既にコードを書いている。また、「4, 5, 7, 9」に関してはnicknameやemailに関してのテストコードとほぼ同じである。
ここでは、「6」を例にあげて解説をしていく。
「nicknameが7文字以上であれば登録できないこと」を確かめるためには、以下のようなコードを記載する。
*【例】6に関するコードに
1 2 3 4 5 |
it "is invalid with a nickname that has more than 7 characters " do
user = build(:user, nickname: "aaaaaaaa")
user.valid?
expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")
end
|
上記の4行目で、文字数の長さに関するエラー文「"is too long (maximum is 6 characters)"」を確かめている。
そのエラー文の中身を確かめる場合は、bundle exec rspecコマンドを打つ。
1 2 3 4 5 6 |
it "is invalid with a nickname that has more than 7 characters " do
user = build(:user, nickname: "aaaaaaaa")
user.valid?
binding.pry
expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")
end
|
1 2 |
pry(#<RSpec::ExampleGroups::User::Create>)> user.errors[:nickname]
=> ["is too long (maximum is 6 characters)"]
|
このようにエラー文の中身を判別することができるので、その後にincludeマッチャを用いて検証を行う。
これと同様の方法で、10の「passwordが5文字以下であれば登録できないこと」も確かめることができる。
なお、今回binding.pry
で出力した["is too long (maximum is 6 characters)"]
は配列に文字列が格納されている。このような場合でもincludeマッチャを用いることはできる。より深く知りたいなら、以下の資料を参照すると良い。
Project: RSpec Expectations 3-9