概要
最近テストコードの重要性を身にしみて分かってきました。
今個人的にRailsでアプリケーションを作っているのですが、deviseを使ったサインアップ機能を実現したとします。
そこで後から、Userモデルにカラムを追加したい!とか、逆にカラムを削除したい!とか、カラムをテーブルから分離させたい!なんてことがありました。
アプリケーションの基幹部分からの変更なので、モデルの構成と機能の部分で齟齬が発生しないか、やっぱり不安になっちゃうんですよね。
そんな時にrspecによるテストコードをサインアップ機能と平行して書いておくと、rspecを走らせるだけで簡単に新しいDB構成の妥当性を確認することができます。
それでエラーが出るなら見直せばいいし、テストが通るならそのままでも大丈夫だと言えるはずです。
自分でまた一から操作するのはめんどくさいですからね・・
で、今回はモデルスペックというMVCのうちmodelに対して行うテストについて書いていこうかと思います。
テストにも種類があって、優先順位や重要度なんかもあったりします。
ごく簡単にまとめられている記事を見つけたので目を通してみてください。すぐ読み終わるはずです。
前提
・Rails 5.2.3
・Ruby 2.5.1
・rspec-rails 3.9.0
準備
modelテストを行う場合、spec/models/user_spec.rbというファイルを編集することになります。
rspecを使うためのセットアップ方法は下記記事を参考にしてみてください。
RailsでのRSpecの設定やら書き方とかいろいろ
テーブル構成
今回は概要で説明したようなUserモデルのサインアップ機能についてモデルスペックを書いてみようかと思います。
Userテーブル
Column | Type | Options |
---|---|---|
string | null: false | |
password | string | null: false |
first_name | string | null: false |
family_name | string | null: false |
まずはテスト要件を書く
いきなりテストコードを書き始める前に、テストしたい内容を書きましょう。
今回のUserテーブルの場合、要求される要件はこんな感じになるかと思います。
要件出しの際は、「正常系」と「異常系」を意識して書くようにしてください。
つまり、「こうだったら問題が起きない場合」と「こうだったら問題が起きる場合」を書き出す、というような感じです。
require 'rails_helper'
RSpec.describe User, type: :model do
it "姓、名、メール、パスワードがある場合、有効である"
it "名がない場合、無効である"
it "姓がない場合、無効である"
it "メールアドレスがない場合、無効である"
it "重複したメールアドレスの場合、無効である"
it "パスワードがない場合、無効である"
end
"姓、名、メール、パスワードがある場合、有効である"テストコード
まずは一番上の「正常系」のテストから書いていきましょう。
簡単な解説はコメントアウトを参照ください。
require 'rails_helper'
RSpec.describe User, type: :model do
it "姓、名、メール、パスワードがある場合、有効である" do
# userのそれぞれのカラムに対して値を入れてオブジェクト化する
user = User.new(
first_name: "tarou",
last_name: "testman",
email: "testman@example.com",
password: "password",
)
# オブジェクトをexpectに渡した時に、有効である(be valid)という意味になります
expect(user).to be_valid
end
it "名がない場合、無効である"
it "姓がない場合、無効である"
it "メールアドレスがない場合、無効である"
it "重複したメールアドレスの場合、無効である"
it "パスワードがない場合、無効である"
end
User.newはオブジェクト化して、その時のカラムの値を引数として渡しています。
railsのコントローラとかで操作するときとおんなじですね!
expect(user).to はrspecでよく出てくる構文です。
これにより()内部に入れたものが現在どう言う状態かチェックできます。
be validはexpect().toの状態が有効であると言う意味になります。
この記述をした状態でbundle exec rspecをするとテストが通るはずです。
ここでいうテストが通るというのは、**「姓、名、メール、パスワードがある場合、有効であることが保証される」**と言うことになります。
この時にbe valid以外の条件で動作をテストして、「テストが通らないこと」を確認することをお勧めします。
failed画面ではexpect内部でどんな状態かなどが表示されます。
モデルテストに限らず、failedの表示を参考にしながらテストコードを書くのが基本かなと思います。
"名がない場合、無効である"テストコード
二つ目のテストコードですが、こんな感じになります。
これまた簡単な説明はコメントアウトに譲ります。
require 'rails_helper'
RSpec.describe User, type: :model do
it "姓、名、メール、パスワードがある場合、有効である" do
# userのそれぞれのカラムに対して値を入れてオブジェクト化する
user = User.new(
first_name: "tarou",
last_name: "testman",
email: "testman@example.com",
password: "password",
)
# オブジェクトをexpectに渡した時に、有効である(be valid)という意味になります
expect(user).to be_valid
end
it "名がない場合、無効である" do
# first_nameでnilを設定する
user = User.new(
first_name: nil,
last_name: "testman",
email: "testman@example.com",
password: "password",
)
# valid?メソッドを呼び出すとエラー検証が行えます。次行のerrorsメソッドを使うために必要です。
user.valid?
# valid?メソッドでfalseであれば、user.errosでどんなerrorを持っているか返してくれます。今回は特に[:first_name]のエラーがみたいのでexpect()内部で指定してあげます。
# 今回は"can't be blank"というエラーを含んでいる(include)しているはずという記述になります。複数含む場合ももちろんあります。
expect(user.errors[:first_name]).to include("can't be blank")
end
it "姓がない場合、無効である"
it "メールアドレスがない場合、無効である"
it "重複したメールアドレスの場合、無効である"
it "パスワードがない場合、無効である"
end
こんな感じでテストコードを書いていきます。
expect().toの後ろに書いている判定条件(マッチャと言います)はかなり多岐に渡ります。
この辺り参考にしてみてください。
使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」
これでbundle exec rspecをしてみるとテストが通るはず・・なんですが、テストが通らない場合。
userモデルのfirst_nameに対して、「nilはダメ!」っていうバリデーションが記述されていない可能性が高いです。
なのでこんな感じでuserモデルに追記しましょう。
class User < ApplicationRecord
(省略)
validates :first_name, presence: true
(省略)
end
これでテストを動かせればテストが通るはずです。
ここでいうテストが通るというのは、**「first_nameがnilの場合、無効であることが保証される」**と言うことになります。
ここでもやっぱりinclude以外の条件も試してみて、「テストが通らないこと」を確認しましょう。
こんな感じで、テストコードを書く⇨試す⇨テストが通る通らないを検証⇨対象モデルのバリデーションを確認⇨書く⇨試す...ていうのをひたすら繰り返していきます。
User.new長くね?問題
これから先続きのテストコードも書いていくんですが、User.newを一つ一つの検証ごとに書き加えることになります。
それじゃモデルテストのコードがガンガン長くなっていってしまいますよね・・
そこで、FactoryBotというgemを使いましょう。
セッティングや使い方は今回は割愛させてください。下記を参照ください。
RailsでのRSpecの設定やら書き方とかいろいろ
これ以降はFactoryBotを適用した記述になります。ご容赦ください
テストコード書き終わり
僕が書いた場合、最終的にはこんなテストコードになるかとおもいます。
FactoryBot.createとFactoryBot.buildの違いは、テストDBに値を保持するかどうか。
createは値を保持して、buildはオブジェクト化するだけです。
emailの重複を確認したかったので、DBに値が必要だったというわけですね。
テストコードはこんな感じ
require 'rails_helper'
RSpec.describe User, type: :model do
it "姓、名、メール、パスワードがある場合、有効である" do
user = FactoryBot.build(:user)
expect(user).to be_valid
end
it "名がない場合、無効である" do
user = FactoryBot.build(:user, first_name: nil)
user.valid?
expect(user.errors[:first_name]).to include("can't be blank")
end
it "姓がない場合、無効である" do
user = FactoryBot.build(:user, family_name: nil)
user.valid?
expect(user.errors[:family_name]).to include("can't be blank")
end
it "メールアドレスがない場合、無効である" do
user = FactoryBot.build(:user, email: nil)
user.valid?
expect(user.errors[:email]).to include("can't be blank")
end
it "重複したメールアドレスの場合、無効である" do
user1 = FactoryBot.create(:user)
user2 = FactoryBot.build(:user)
user2.valid?
expect(user2.errors[:email]).to include("has already been taken")
end
it "パスワードがない場合、無効である"
user = FactoryBot.build(:user)
user.valid?
expect(user.errors[:password]).to include("can't be blank")
end
end
モデルバリデーションはこんな感じ
class User < ApplicationRecord
(省略)
validates :first_name, presence: true
validates :family_name, presence: true
validates :password, presence: true
validates :email, presence: true,uniqueness: true
(省略)
end
まとめ
長かった・・疲れた・・・
色々と解説の足りないところがあるけれど、コメントとかで質問があれば頑張って答えます。
ちなみにまだまだ検証しなければいけない内容があるのはお気づきでしょうか?
例えばemailだったら、aiueo@example.comみたいな@以前、@以後だとかアルファベットと数字じゃないとダメ!とか、family_nameは最大何文字まで!とか、passwordは何文字以上じゃないとダメ!とかありますよね
そういったものもモデルスペックの対象です。
モデルにバリデーションを追記して、その都度テスト・・・という流れになります。
根気のいる作業ではありますが、一度書いとくとあとが楽です。