前提条件
今回は新規ユーザー登録に関するモデルの単体テストを実装していきながら、方法をアウトプットします。ユーザー登録の仕様書は以下のとおり。
ニックネームが必須
メールアドレスは一意である
メールアドレスは@とドメインを含む必要がある
パスワードが必須
パスワードは7文字以上
パスワードは確認用を含めて2回入力する
ユーザー本名が、名字と名前でそれぞれ必須
ユーザー本名は全角で入力させる
ユーザー本名のフリガナが、名字と名前でそれぞれ必須
ユーザー本名のフリガナは全角で入力させる
生年月日が必須
マイグレーションファイルは以下のとおり。
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
## Database authenticatable
t.string :nickname, null: false
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.string :family_name, null: false
t.string :family_name_kana, null: false
t.string :first_name, null: false
t.string :first_name_kana, null: false
t.date :birth_day, null: false
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
# t.integer :sign_in_count, default: 0, null: false
# t.datetime :current_sign_in_at
# t.datetime :last_sign_in_at
# t.string :current_sign_in_ip
# t.string :last_sign_in_ip
## Confirmable
# t.string :confirmation_token
# t.datetime :confirmed_at
# t.datetime :confirmation_sent_at
# t.string :unconfirmed_email # Only if using reconfirmable
## Lockable
# t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
# add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true
end
end
モデルは以下のとおり。
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
validates :encrypted_password, presence: true
validates :family_name, presence: true
validates :family_name_kana, presence: true
validates :first_name, presence: true
validates :first_name_kana, presence: true
validates :birth_day, presence: true
# アソシエーション
has_many :cards, dependent: :destroy
has_many :shipping_infos, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :items
end
実装手順
① RSpecの準備
② RSpecの設定
③ factory_botを導入
④ factory_botの実装
⑤ 正規表現の記述
⑥ テストコードの実装
⑦ テスコードの実行
RSpecの準備
まずGemをインストールします。
group :development, :test do
# 省略
gem 'rspec-rails'
end
group :development do
gem 'web-console'
end
# web_console は既に記述がある場合は、記述場所を移動するだけでOK。追記してしまうと重複してしまう可能性があるため注意してください。
Gemfileを編集したら、忘れずにbundle installを実行。
RSpecの設定
続いてRSpecの基本設定を行う。
まずはRSpec用の設定ファイルを作成する。
$ rails g rspec:install
# 以下のようにファイルが作成されれば成功 ▼
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
次に、.rspec
に以下を追加。
--format documentation
続いて、RSpec用ディレクトリを設定します。
RSpecによるテストコードが書かれたファイルのことを、specファイルと呼び、全てのspecファイルは、先程のrails g rspec:install
コマンドで生成されたspecディレクトリの中に格納するので、ここで作成しておきましょう。
モデルに関するテスト用ファイルであればspec/models/以下に、コントローラーに関するテスト用ファイルであればspec/controllers/以下に格納されます。appディレクトリ以下にあるテストの対象となるコードの在り処は全てこのディレクトリです。
specファイルは対応するクラスを持つファイル名_spec.rbという名前になります。今回はまず「user.rb」に関するspecファイルを作成するので、その場合の名前は「user_spec.rb」
になります。
ディレクトリの作成が完了したら、この時点で一度RSpecが正常に利用できるかの確認を行いましょう。
$ bundle exec rspec
# 以下のような結果になれば正常に動作しているのでOK
No examples found.
Finished in 0.00031 seconds (files took 0.19956 seconds to load)
0 examples, 0 failures
factory_botを導入
続いてfactory_botを導入します。
Gemをインストール。
group :development, :test do
# 省略
gem 'rspec-rails'
gem 'factory_bot_rails'
end
その後、bundle installを実行。
factory_botの実装
次に、specディレクトリ直下にfactories
というディレクトリを作成し,その中に、作成したインスタンスの複数形のファイル名でRubyのファイルを作成します。今回の場合は、users.rb
になります。
users.rb
を以下のように編集。
FactoryBot.define do
factory :user do
nickname {"tarou"}
email {"sample@gmail.com"}
password {"1234567"}
encrypted_password {"1234567"}
family_name {"山田"}
family_name_kana {"ヤマダ"}
first_name {"太郎"}
first_name_kana {"タロウ"}
birth_day {"2000-01-01"}
end
end
この記述は、仕様書に沿った内容をダミーデーターとして作成しており、こうすると、specファイルの中で特定のメソッド(buildやcreate)により簡単にインスタンスを生成したり、DBに保存したりできるようになります。
正規表現記述
続いて、仕様書に沿って、バリデーションへ正規表現を設定していきます。
必要な正規表現は以下のとおり。
パスワードは7文字以上
ユーザー本名は全角で入力させる
ユーザー本名のフリガナは全角で入力させる
これらを踏まえてモデルへ記述すると以下のとおり。
class User < ApplicationRecord
# 省略
validates :nickname, presence: true
validates :encrypted_password, presence: true, length: { minimum: 7 } # ここが文字数の正規表現
validates :family_name, presence: true, format: {with: /\A[ぁ-んァ-ン一-龥]/ } # ここがユーザー本名全角の正規表現
validates :family_name_kana, presence: true, format: {with: /\A[ァ-ヶー-]+\z/ } # ここがフリガナ全角の正規表現
validates :first_name, presence: true, format: {with: /\A[ぁ-んァ-ン一-龥]/ } # ここがユーザー本名全角の正規表現
validates :first_name_kana, presence: true, format: {with: /\A[ァ-ヶー-]+\z/ } # ここがフリガナ全角の正規表現
# 省略
end
Rubyの正規表現は下記サイトを参考にしてください。
https://gist.github.com/nashirox/38323d5b51063ede1d41
テストコードの実装
それではいよいよテストコードの実装です!
テスコードを記述するときに心掛ける点は以下のとおり。
①各exampleで期待する値は1つ
②期待する結果をはっきりわかりやすく記述
③起きて欲しいことと起きてほしくないこと両方をテストする
④可読性を考えつつ、適度にDRYにする
user_spec.rbに以下のコードを記述。
require 'rails_helper'
describe User do
describe '#create' do
end
end
今回はUserモデルの単体テストなのでdescribe User do
となり、新規ユーザーを作成するのでdescribe '#create' do
としており、2行目のdescribの中にテストコードを記述します。
まず、全ての項目の入力が存在すれば登録できることをテストします。
コードとその解説は以下の通り。
require 'rails_helper'
describe User do
describe '#create' do
# 入力されている場合のテスト ▼
it "全ての項目の入力が存在すれば登録できること" do # テストしたいことの内容
user = build(:user) # 変数userにbuildメソッドを使用して、factory_botのダミーデータを代入
expect(user).to be_valid # 変数userの情報で登録がされるかどうかのテストを実行
end
end
end
続いてテストすることは以下の通り。
nicknameがない場合は登録できないこと
emailがない場合は登録できないこと
passwordがない場合は登録できないこと
encrypted_passwordがない場合は登録できないこと
family_nameがない場合は登録できないこと
family_name_kanaがない場合は登録できないこと
first_nameがない場合は登録できないこと
first_name_kanaがない場合は登録できないこと
birth_dayがない場合は登録できないこと
これらの条件は仕様書の必須項目(null:false, presence: true)をテストする為のものです。
それでは記述していきましょう。解説はnicknameがない場合は登録できないことのテスト箇所で行います。
require 'rails_helper'
describe User do
describe '#create' do
# 入力されている場合のテスト ▼
it "全ての項目の入力が存在すれば登録できること" do
user = build(:user)
expect(user).to be_valid
end
# nul:false, presence: true のテスト ▼
it "nicknameがない場合は登録できないこと" do # テストしたいことの内容
user = build(:user, nickname: nil) # 変数userにbuildメソッドを使用して、factory_botのダミーデータを代入(今回の場合は意図的にnicknameの値をからに設定)
user.valid? #バリデーションメソッドを使用して「バリデーションにより保存ができない状態であるか」をテスト
expect(user.errors[:nickname]).to include("を入力してください") # errorsメソッドを使用して、「バリデーションにより保存ができない状態である場合なぜできないのか」を確認し、.to include("を入力してください")でエラー文を記述(今回のRailsを日本語化しているのでエラー文も日本語)
end
it "emailがない場合は登録できないこと" do
user = build(:user, email: nil)
user.valid?
expect(user.errors[:email]).to include("を入力してください")
end
it "passwordがない場合は登録できないこと" do
user = build(:user, password: nil)
user.valid?
expect(user.errors[:password]).to include("を入力してください")
end
it "encrypted_passwordがない場合は登録できないこと" do
user = build(:user, encrypted_password: nil)
user.valid?
expect(user.errors[:encrypted_password]).to include("を入力してください")
end
it "family_nameがない場合は登録できないこと" do
user = build(:user, family_name: nil)
user.valid?
expect(user.errors[:family_name]).to include("を入力してください")
end
it "family_name_kanaがない場合は登録できないこと" do
user = build(:user, family_name_kana: nil)
user.valid?
expect(user.errors[:family_name_kana]).to include("を入力してください")
end
it "first_nameがない場合は登録できないこと" do
user = build(:user, first_name: nil)
user.valid?
expect(user.errors[:first_name]).to include("を入力してください")
end
it "first_name_kanaがない場合は登録できないこと" do
user = build(:user, first_name_kana: nil)
user.valid?
expect(user.errors[:first_name_kana]).to include("を入力してください")
end
it "birth_dayがない場合は登録できないこと" do
user = build(:user, birth_day: nil)
user.valid?
expect(user.errors[:birth_day]).to include("を入力してください")
end
end
end
続いて、emailの一意性制約のテストを行います。
require 'rails_helper'
describe User do
describe '#create' do
# 省略
# email 一意性制約のテスト ▼
it "重複したemailが存在する場合登録できないこと" do
user = create(:user) # createメソッドを使用して変数userとデータベースにfactory_botのダミーデータを保存
another_user = build(:user, email: user.email) # 2人目のanother_userを変数として作成し、buildメソッドを使用して、意図的にemailの内容を重複させる
another_user.valid? # another_userの「バリデーションにより保存ができない状態であるか」をテスト
expect(another_user.errors[:email]).to include("はすでに存在します") # errorsメソッドを使用して、emailの「バリデーションにより保存ができない状態である場合なぜできないのか」を確認し、その原因のエラー文を記述
end
end
end
続いて、確認用パスワードがなければ登録できないテストを実行します。
require 'rails_helper'
describe User do
describe '#create' do
# 省略
# 確認用パスワードが必要であるテスト ▼
it "passwordが存在してもencrypted_passwordがない場合は登録できないこと" do
user = build(:user, encrypted_password: "") # 意図的に確認用パスワードに値を空にする
user.valid?
expect(user.errors[:encrypted_password]).to include("を入力してください", "は7文字以上で入力してください")
end
end
end
ここまでで、以下のテストは完了しました。
# 完了したテスト▼
ニックネームが必須 # 完了
メールアドレスは一意である # 完了
メールアドレスは@とドメインを含む必要がある # 完了
パスワードが必須 # 完了
パスワードは確認用を含めて2回入力する # 完了
ユーザー本名が、名字と名前でそれぞれ必須 # 完了
ユーザー本名のフリガナが、名字と名前でそれぞれ必須 # 完了
生年月日が必須 # 完了
# これから実装するテスト▼
パスワードは7文字以上
ユーザー本名は全角で入力させる
ユーザー本名のフリガナは全角で入力させる
それでは、ここからは先ほど記述した正規表現のテストを行います。
まずパスワードは7文字以上のテストから。
起きて欲しいことと起きてほしくないことの両方をテストします。
require 'rails_helper'
describe User do
describe '#create' do
# 省略
# パスワードの文字数テスト ▼
it "passwordが7文字以上であれば登録できること" do
user = build(:user, password: "1234567", encrypted_password: "1234567") # buildメソッドを使用して7文字のパスワードを設定
expect(user).to be_valid
end
it "passwordが7文字以下であれば登録できないこと" do
user = build(:user, password: "123456", encrypted_password: "123456") # 意図的に6文字のパスワードを設定してエラーが出るかをテスト
user.valid?
expect(user.errors[:encrypted_password]).to include("は7文字以上で入力してください")
end
end
end
最後に全角入力と全角カナ入力のテストを行います。
require 'rails_helper'
describe User do
describe '#create' do
# 省略
# 名前全角入力のテスト ▼
it 'family_nameが全角入力でなければ登録できないこと' do
user = build(:user, family_name: "アイウエオ") # 意図的に半角入力を行いエラーを発生させる
user.valid?
expect(user.errors[:family_name]).to include("は不正な値です")
end
it 'first_nameが全角入力でなければ登録できないこと' do
user = build(:user, first_name: "アイウエオ") # 意図的に半角入力を行いエラーを発生させる
user.valid?
expect(user.errors[:first_name]).to include("は不正な値です")
end
# カタカナ全角入力のテスト ▼
it 'family_name_kanaが全角カタカナでなければ登録できないこと' do
user = build(:user, family_name_kana: "あいうえお") # 意図的にひらがな入力を行いエラーを発生させる
user.valid?
expect(user.errors[:family_name_kana]).to include("は不正な値です")
end
it 'first_name_kanaが全角カタカナでなければ登録できないこと' do
user = build(:user, first_name_kana: "あいうえお") # 意図的にひらがな入力を行いエラーを発生させる
user.valid?
expect(user.errors[:first_name_kana]).to include("は不正な値です")
end
end
end
これで仕様書に沿った機能のテストは以上です。
最終的には以下のようになります。
require 'rails_helper'
describe User do
describe '#create' do
# 入力されている場合のテスト ▼
it "全ての項目の入力が存在すれば登録できること" do
user = build(:user)
expect(user).to be_valid
end
# nul:false, presence: true のテスト ▼
it "nicknameがない場合は登録できないこと" do
user = build(:user, nickname: nil)
user.valid?
expect(user.errors[:nickname]).to include("を入力してください")
end
it "emailがない場合は登録できないこと" do
user = build(:user, email: nil)
user.valid?
expect(user.errors[:email]).to include("を入力してください")
end
it "passwordがない場合は登録できないこと" do
user = build(:user, password: nil)
user.valid?
expect(user.errors[:password]).to include("を入力してください")
end
it "encrypted_passwordがない場合は登録できないこと" do
user = build(:user, encrypted_password: nil)
user.valid?
expect(user.errors[:encrypted_password]).to include("を入力してください")
end
it "family_nameがない場合は登録できないこと" do
user = build(:user, family_name: nil)
user.valid?
expect(user.errors[:family_name]).to include("を入力してください")
end
it "family_name_kanaがない場合は登録できないこと" do
user = build(:user, family_name_kana: nil)
user.valid?
expect(user.errors[:family_name_kana]).to include("を入力してください")
end
it "first_nameがない場合は登録できないこと" do
user = build(:user, first_name: nil)
user.valid?
expect(user.errors[:first_name]).to include("を入力してください")
end
it "first_name_kanaがない場合は登録できないこと" do
user = build(:user, first_name_kana: nil)
user.valid?
expect(user.errors[:first_name_kana]).to include("を入力してください")
end
it "birth_dayがない場合は登録できないこと" do
user = build(:user, birth_day: nil)
user.valid?
expect(user.errors[:birth_day]).to include("を入力してください")
end
# パスワードの文字数テスト ▼
it "passwordが7文字以上であれば登録できること" do
user = build(:user, password: "1234567", encrypted_password: "1234567")
expect(user).to be_valid
end
it "passwordが7文字以下であれば登録できないこと" do
user = build(:user, password: "123456", encrypted_password: "123456")
user.valid?
expect(user.errors[:encrypted_password]).to include("は7文字以上で入力してください")
end
# email 一意性制約のテスト ▼
it "重複したemailが存在する場合登録できないこと" do
user = create(:user)
another_user = build(:user, email: user.email)
another_user.valid?
expect(another_user.errors[:email]).to include("はすでに存在します")
end
# 確認用パスワードが必要であるテスト ▼
it "passwordが存在してもencrypted_passwordがない場合は登録できないこと" do
user = build(:user, encrypted_password: "")
user.valid?
expect(user.errors[:encrypted_password]).to include("を入力してください", "は7文字以上で入力してください")
end
# 本人確認名前全角入力のテスト ▼
it 'family_nameが全角入力でなければ登録できないこと' do
user = build(:user, family_name: "アイウエオ")
user.valid?
expect(user.errors[:family_name]).to include("は不正な値です")
end
it 'first_nameが全角入力でなければ登録できないこと' do
user = build(:user, first_name: "アイウエオ")
user.valid?
expect(user.errors[:first_name]).to include("は不正な値です")
end
# 本人確認カタカナ全角入力のテスト ▼
it 'family_name_kanaが全角カタカナでなければ登録できないこと' do
user = build(:user, family_name_kana: "あいうえお")
user.valid?
expect(user.errors[:family_name_kana]).to include("は不正な値です")
end
it 'first_name_kanaが全角カタカナでなければ登録できないこと' do
user = build(:user, first_name_kana: "あいうえお")
user.valid?
expect(user.errors[:first_name_kana]).to include("は不正な値です")
end
end
end
## テスコードの実行
テストを記述したアプリのディレクトリのターミナルにて下記コマンドを実行。
$ bundle exec rspec
すると下記画像のように処理が行われればテストが成功しています。
最後に
モデルの単体テストは基本文法と、エラーを発生させる処理と発生させない処理を把握し、それぞれのメソッドを理解することができれば新たに機能が追加された場合でもコードを記述できると思います。
何か気になる点がございましたらコメント欄にてお申し付けください!
最後までご覧いただきありがとうございました!