User モデルのテスト
前回、devise を用いて User モデルを作成しました。
実際に User モデルを使用する前に、User モデルが機能することを確認するためのテストを作成しておきましょう。
また、User モデルの一部の属性についてはバリデーションを設定しつつ、そのテストも同時に作成します。
テスト関連ファイルの確認
前回 rails g devise User
を実行した際、テストに関連する以下のファイルが作成されていました。
- spec/factories/users.rb
- spec/models/user_spec.rb
今回はこれらを修正し、最後に bin/rspec
実行時に全てのテストが通ることを目標とします。
Factory ファイルの修正
spec/factories/users.rb
は、以前導入した FactoryBot を用い、テストで User を簡単に作成するためのファイルです。
こちらを以下のように修正してください。
spec/factories/users.rb
FactoryBot.define do
factory :user do
nickname { 'テスト太郎' }
sequence :email do |n|
"test#{n}@example.com"
end
password { '111111' }
password_confirmation { '111111' }
end
end
ユニークキーである email の一部だけが異なる、複数の User を作成する Factory となっています。
User 生成&取得のテスト
次に User モデルの Spec であるspec/models/user_spec.rb
を修正します。
まずは基本的なところとして、FactoryBot で生成した User を User.first
で取得し、
取得した User が想定通りの属性(nickname, email) を持っていることを確認するテストを用意しましょう。
spec/models/user_spec.rb
require 'rails_helper'
describe User do
let(:nickname) { 'テスト太郎' }
let(:email) { 'test@example.com' }
describe '.first' do
before do
create(:user, nickname: nickname, email: email)
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
end
end
これでテストを実行してみます。
テストを実行する際、比較的時間がかかる System Spec まで一緒に実行すると開発スピードが落ちるため、User モデルの Spec だけを実行します。
特定のテストだけを実行したい場合は、bin/rspec
の後に半角スペースを挟み、さらに実行したいテストのファイル名を渡します。
1 つだけですが、テストが通っていることを確認してください。
$ bin/rspec spec/models/user_spec.rb
...
User
.first
事前に作成した通りのUserを返す
Finished in 0.06006 seconds (files took 0.5011 seconds to load)
1 example, 0 failures
User.nickname のバリデーション(文字数)
ここでは User の nickname 属性に文字数制限のバリデーションを設定していきます。
その際、バリデーションのテストを作成し、それから実際にバリデーションを設定します。
※このように要件を満たすためのテストを事前に作成してから実装をすることをテスト駆動開発といいます。
バリデーションのテストを追加
今回は、ニックネームは 20 文字までという制限をかけていきます。
そこで、先にこのバリデーションに関するテストを作成しておき、それから実際に User モデルにバリデーションを設定します。
先にテストを書いておくことで仕様が明確になり、確実にバリデーションを実装することができます。
テストでは以下の 2 つを追加します。
- User.nickname が 20 文字の場合、User オブジェクトが有効であること
- User.nickname が 21 文字の場合、User オブジェクトが無効であること
これを満たすようなテストとして、.first
という describe とはまた別に .validation
という describe (分け方) のテストを追加します。
user_spec.rb の全文は以下のようになります。
spec/models/user_spec.rb
require 'rails_helper'
describe User do
let(:nickname) { 'テスト太郎' }
let(:email) { 'test@example.com' }
let(:password) { '12345678' }
let(:user) { User.new(nickname: nickname, email: email, password: password, password_confirmation: password) }
describe '.first' do
before do
create(:user, nickname: nickname, email: email)
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
end
describe 'validation' do
describe 'nickname属性' do
describe '文字数制限の検証' do
context 'nicknameが20文字以下の場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてと' } # 20文字
it 'User オブジェクトは有効である' do
expect(user.valid?).to be(true)
end
end
context 'nicknameが20文字を超える場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてとな' } # 21文字
it 'User オブジェクトは無効である' do
user.valid?
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include('is too long (maximum is 20 characters)')
end
end
end
end
end
end
user
という変数に作成した User オブジェクトを格納しています。
有効な user
については user.valid?
が true
となることを確認しています。
一方、無効な user
についてはuser.valid?
実行時にuser.errros
にエラー内容が入ることを利用したテストとなっています。
まだバリデーションを設定はしていないので、テストは失敗することを確認してください。
$ bin/rspec spec/models/user_spec.rb
...
Finished in 0.11684 seconds (files took 0.50221 seconds to load)
3 examples, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:35 # User validation nickname文字数制限の検証 nicknameが20文字を超える場合 User オブジェクトは無効である
バリデーションを設定しても通るテストについてはそのままですが、21 文字以上の nickname というテストは失敗しています。
バリデーションを追加
では User モデルにバリデーションを追加しましょう。
app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
validates :nickname, length: { maximum: 20 } # 追加
end
nickname について、長さの最大が 20 文字であるという制約のバリデーションを追加しました。
テスト実行
バリデーションを追加したため、この時点でテストが通るはずです。
テストを再度実行し、User モデルのテストはすべて通ることを確認しましょう。
$ bin/rspec spec/models/user_spec.rb
...
User
.first
事前に作成した通りのUserを返す
validation
nickname属性
文字数制限の検証
nicknameが20文字以下の場合
User オブジェクトは有効である
nicknameが20文字を超える場合
User オブジェクトは無効である
Finished in 0.11225 seconds (files took 0.4578 seconds to load)
3 examples, 0 failures
バリデーションを設定し、テストが通ることも確認できたので一旦コミットしておきます。
$ git add .
$ git commit -m "User.nickname の文字数に関するバリデーションを追加"
User.nickname のバリデーション(空欄ではないこと)
続いて、User.nickname は空欄ではないことのバリデーションを追加してあげます。
バリデーションのテストを追加
文字数のバリデーションと同じく、テストを先に追加しましょう。
nickname が空欄である User オブジェクトを作り、user.valid?
をかけた時に空欄を許可しないことを示すエラーメッセージが含まれることを確認します。
spec/models/user_spec.rb
describe 'nickname存在性の検証' do
context 'nicknameが空欄の場合' do
let(:nickname) { '' }
it 'User オブジェクトは無効である' do
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include("can't be blank")
end
end
end
なお、上記テストの挿入位置ですが事前に追加していた describe '文字数制限の検証' do
と同列となるように追記します。
これは、nickname 属性に関するいくつかの検証のうち「文字数制限」と「存在性」(空欄ではないこと) の検証が同列であることを明確にするためです。
こうしておくと、テストを実行した際にインデントが揃ったテストどうしは同列であることが分かるため、仕様やテストの意図が分かりやすくなります。
さて、このタイミングでは追加したテストだけが失敗することを確認しましょう。
$ bin/rspec spec/models/user_spec.rb
...
User
.first
事前に作成した通りのUserを返す
validation
nickname属性
文字数制限の検証
nicknameが20文字以下の場合
User オブジェクトは有効である
nicknameが20文字を超える場合
User オブジェクトは無効である
存在性の検証
nicknameが空欄の場合
User オブジェクトは無効である (FAILED - 1)
...
Failures:
1) User validation nickname属性 存在性の検証 nicknameが空欄の場合 User オブジェクトは無効である
Failure/Error: expect(user.valid?).to be(false)
expected false
got true
...
nickname が空欄であり、正しくない User オブジェクトについて user.valid?
は本来 false
を返すことをテストは期待しています。
しかし、そのバリデーションをかけていないため、まだ user.valid?
は true
を返しています。
結果として、テストは現時点では失敗することなります。
テスト駆動開発では最初に「失敗するテスト」を書くことが重要です。
逆に、この時点でテストが通ってしまうようであれば、正しいテストを書けていませんので注意しましょう。
続けて、このテストが通るようにバリデーションを追加しましょう。
バリデーションを追加
テストが書けたので、nickname の空欄を強制するバリデーションを追加します。
presence: true
を追加することで実現できます。
app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
validates :nickname, length: { maximum: 20 }
validates :nickname, presence: true # 追加
end
なお、本当は nickname に関するバリデーションは 1 行で書けるのですが、
テスト駆動開発の流れに則り最後にリファクタリングを行うため、あえて改善の余地があるコードのままにしておきます。
テスト実行
バリデーションを追加したので、今度はテストが通ることを確認してください。
$ bin/rspec spec/models/user_spec.rb
DEBUGGER: Attaching after process 50812 fork to child process 50861
Running via Spring preloader in process 50861
User
.first
事前に作成した通りのUserを返す
validation
nickname属性
文字数制限の検証
nicknameが20文字以下の場合
User オブジェクトは有効である
nicknameが20文字を超える場合
User オブジェクトは無効である
存在性の検証
nicknameが空欄の場合
User オブジェクトは無効である
Finished in 0.08105 seconds (files took 0.4687 seconds to load)
4 examples, 0 failures
テストのリファクタリング
さて、テストが通りましたのでリファクタリング(以下、リファクタ)をしていきましょう。
リファクタ作業では、処理による最終的な結果を変えることなく重複を省いたり、設計を変えたりしてコードの改善を行います。
テスト駆動開発では
- 失敗するテストを書く
- 実装してテストを通す
- リファクタをしてコードを改善する
という流れで実装を進めていき、品質を高めていきます。
では実際にリファクタをしていきましょう。
User モデルのコードで nickname のバリデーションはわざわざ 2 行に分けていましたが、
先ほど軽く触れた通り 1 行でまとめられるのでリファクタしてしまいます。
app/models/user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
validates :nickname, presence: true, length: { maximum: 20 } # 変更
end
裏ファクタを終えたので最後にテストを再実行し、すべて通ることを確認します。
$ bin/rspec spec/models/user_spec.rb
...
4 examples, 0 failures
「テストを通している=動作の保証ができている」という流れで進めていたため、
テストさえ通るようにすれば、安心してリファクタリングできますね!
これで一通りのテスト駆動開発の流れは掴めたかと思います。
では最後にすべてのテストを走らせ、コケているテストが無いことを確認しておきましょう。
開発を進めていくうちに予期せぬ箇所に影響することもあるため、最終的にはすべてのテストを走らせておくことが重要です。
$ bin/rspec
...
Finished in 2.61 seconds (files took 0.34742 seconds to load)
6 examples, 0 failures
変更をコミット
問題ないことを確認できたら、ここまでの変更をコミットし、プッシュもしておきます。
$ git add .
$ git commit -m "User.nicknameのバリデーションを追加"
$ git push
補足
テストコードの全文
今回はテストコードについて何度か修正を行ったため、全貌を掴むのに苦労したかと思います。
念のためテストの全文を載せておきますので、自分のテストと照らし合わせておきましょう。
spec/models/user_spec.rb
require 'rails_helper'
describe User do
let(:nickname) { 'テスト太郎' }
let(:email) { 'test@example.com' }
let(:password) { '12345678' }
let(:user) { User.new(nickname: nickname, email: email, password: password, password_confirmation: password) } # 変数に格納
describe '.first' do
before do
create(:user, nickname: nickname, email: email)
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
end
describe 'validation' do
describe 'nickname属性' do
describe '文字数制限の検証' do
context 'nicknameが20文字以下の場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてと' } # 20文字
it 'User オブジェクトは有効である' do
expect(user.valid?).to be(true)
end
end
context 'nicknameが20文字を超える場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてとな' } # 21文字
it 'User オブジェクトは無効である' do
user.valid?
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include('is too long (maximum is 20 characters)')
end
end
end
describe '存在性の検証' do
context 'nicknameが空欄の場合' do
let(:nickname) { '' }
it 'User オブジェクトは無効である' do
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include("can't be blank")
end
end
end
end
end
end
email / password のバリデーションについて
nickname についてのみバリデーションを設定しましたが、email や password のバリデーションについても気になる方がいるかと思います。
しかし、devise で最初から用意されているカラムについては最低限のバリデーションが設定されているため、
存在性(空欄ではないこと)のような基本的なバリデーションは不要です。
実際、email を空欄にした User を作成するようなテストを書いてみると "Email can't be blank" と出力されます。
Failure/Error: create(:user, nickname: '太郎', email: '')
ActiveRecord::RecordInvalid:
Validation failed: Email can't be blank
このように gem で最初から用意されているメソッド等については
既に gem 側でテストが書かれているため、新たにテストを書かないのが一般的です。
※バリデーションメッセージが画面に表示されるという要件については、今後作っていく System Spec にてテストします。
宿題
RSpec の書き方おさらい
以前、RSpec の章で基本的な RSpec の書き方については主に宿題で学んでいただきましたが、
今回使用したいくつかの書き方は個別に復習しておきましょう。
- let について
- describe / context / it の使い分け
- expect とマッチャ
バリデーション
Rails でモデルに制約をかけることをバリデーションといいますが、もしご存じではない方はこの機会に学んでおきましょう。
テスト駆動開発
今回、失敗するテストを書く => テストを通す => テストをリファクタする、というテスト駆動開発の流れで進めました。
さらっと流れだけを説明しましたが、その考え方自体は大変奥深いものです。
テストを書くことの重要性をご理解いただく意味でも、テスト駆動開発の概念や経緯については一度目を通しておくと勉強になります。