今回はモデルのテストについて書いていこうと思います。
この記事でわかること
- Railsのモデルをテストする方法について。
- ユーザ名、emailに関してテストするべきことについて。
テスト
何をテストするか
今回は、ユーザ名、emailのレコードをデータベースに保存する際に検証する必要がある以下の項目についてのテストを書いていこうと思います。- 有効性
- 存在性
- 無効性
- 一意性
有効性のテスト
rails generateコマンドで生成されたファイルに、user.rb, user_test.rbがあります。それぞれ最初は以下のようになっています。class User < ApplicationRecord
end
require 'test_helper'
class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
user.rbには、どんなレコードが有効であるかを定義していきます。user_test.rbにはテストを書いていきます。
まずは、以下のようなコードを書きます。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(:name "Katsuki Sanada", :email "sanada@example.com")
end
test "should be valid" do
assert @user.valid?
end
end
このコードは、テストを実行する際に使用するレコードをsetupメソッドで定義します。@userはインスタンス変数なので、ファイル内のテストで使用することができます。
テスト"should be valid"では、setupで作成されたレコードが有効であるかを検証します。
では、早速有効性についてのテストを書いていきます。ユーザ名とemailのレコードが空だとよくないので、それをテストします。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(:name "Katsuki Sanada", :email "sanada@example.com")
end
,
,
,
#nameが空ではない
test "name should be present" do
@user.name = ' '
assert_not @user.valid?
end
#emailが空ではない
test "email should be present" do
@user.email = ' '
assert_not @user.valid?
end
end
assert_notは引数の返り値がfalseの時にtrueを返します。今回は、name,emailが空の時にそれらが有効でないことを確認します。なので、assert_notを使用します。
しかし、このままだとテストは成功しません。
なぜなら、何を有効にするかという定義をuser.rbで行っていないからです。そこで、以下のコードを追加します。
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true
end
これによって、nameとemail属性は空でない時に有効になりました。ちなみに、validatesはメソッドで、:nameとpresence:は引数になります。presence:は「空でない」という意味です。それがtrueなので、「:nameが空でない」と定義されます。
存在性のテスト
さて、空でないことを確認することができました。次は、それぞれのレコードの長さについて制限を加えていきます。ユーザ名とメールアドレスはwebサイトに表示するので、長さに制限を加える必要があります。今回は、ユーザ名とメールアドレスをそれぞれ51文字未満と256文字未満に制限します。
まずはテストを書きます。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(:name "Katsuki Sanada", :email "sanada@example.com")
end
,
,
,
#ユーザ名の長さに制限をつける
test "name should not be too long" do
@user.name = 'a' * 51
assert_not @user.valid?
end
#emailの長さに制限をつける
test "emails should not be too long" do
@user.email = 'a' * 244 + '@example.com' #256文字
assert_not @user.valid?
end
end
このままではテストが通ってしまうのでuser.rbに制限を定義します。
class User < ApplicationRecord
validates :name, presence: true, length { maximum: 50 }
validates :email, presence: true, length { maximum: 255 }
end
length { maximum: 50 }で最大文字数を制限しています。
これで、nameとemailの文字数を制限することができました。
メールアドレスを検証するテスト
メールアドレスの検証には以下のコードを使います。class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(:name "Katsuki Sanada", :email "sanada@example.com")
end
,
,
,
#有効なemailについてのテスト
test "email validation should accept valid addresses" do
valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
first.last@foo.jp alice+bob@baz.cn]
valid_addresses.each do |valid_address|
@user.email = valid_address
assert @user.valid?, "#{valid_address.inspect} should be valid"
end
end
#無効なemailについてのテスト
test "email validation should reject invalid addresses" do
invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
foo@bar_baz.com foo@bar+baz.com]
invalid_addresses.each do |invalid_address|
@user.email = invalid_address
assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
end
end
%w[]では以下のような処理が行われています。
> %w[a b v]
=> ["a", "b", "v"]
このメソッドによって、入力された内容を配列として出力します。その配列の要素をeachメソッドで取り出して、それぞれについてテストする形になっています。
メールアドレスの認証には正規表現を使用します。
今回使用する正規表現は以下のようになります。
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
意味としては、「英数字、プラス、ハイフン、ドットが少なくとも1つ以上が繰り返された後に@が来て、その後に英数字、ハイフン、ドットが少なくとも1つ以上の後にドットが来て、そのあとは英字が少なくとも一つ以上である。」です。
↓詳しくは次を参照ください。
基本的な正規表現一覧
↓正規表現の確認には以下が便利
Rubular
この正規表現をバリデーションに追加します。
class User < ApplicationRecord
validates :name, presence: true, length { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
end
formatオプションは正規表現を引数に持ちます。定数VALID_EMAIL_REGEXに正規表現を代入して、引数にしています。
これで、正規表現でメールアドレスを有効なものとそうでないものに分けるテストを実装できました。
一意性に関するテスト
一意性をテストするのは、同じメールアドレスが被っていると問題があるからです。(多分)テストコードは次のようになります。
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(:name "Katsuki Sanada", :email "sanada@example.com")
end
,
,
,
#一意性についてのテスト
test "email addresses should be unique" do
duplicate_user = @user.dup
@user.save
assert_not duplicate_user.valid?
end
dupメソッドで@userを複製します。それを変数duplicate_userに代入します。@userはUser.newで作成しているので、データベースにレコードが保存されていません。そのため、saveメソッドで@userをデータベースに保存します。
次に、以下の記述をuser.rbに追加します。
before_save {self.email = email.downcase}
これはbefore_saveメソッドはsaveメソッドが行われる前実行する処理を定義できます。ここでは、saveメソッドのレシーバーのemail属性の文字を全て小文字に変換する処理が書いてあります。これは、メールアドレスは大文字小文字の区別をしないために必要な処理です。データベースに保存する前に全て小文字に変換することで重複を防げます。
また、以下のコードもuser.rbに追加します。
uniqueness: true
このコードによって、重複したメールアドレスは作成することができなくなります。
また以下のようなコードもあります。
uniqueness: { case_sensitive: true }
こちらのコードは、メールアドレスの大文字小文字を区別して保存できるようにします。railsではこちらのコードでも、uniqueness: trueと同様の処理を行ってくれるので問題なしです。
追加後のコードが以下のようになります。
class User < ApplicationRecord
before_save {self.email = email.downcase}
validates :name, presence: true, length: {maximum: 50}
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: {maximum: 255},
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: true }
end
一意性の仕上げ
最後に一意性検証の仕上げをしていきます。
ここまでコードだと、ユーザーがメールアドレス保存ボタンを高速で2回クリックしてしまったときなどに一意性を保てなくなります。この場合だと、1回目のアドレスは保存するけど、2回目は保存してはいけません。
そこで、データベースにインデックスというテーブルを新たに作成します。ここにメールアドレスを保存します。その際にマイグレーションで一意性を保つように指定します。以下を実行します。
rails generate migration add_index_to_users_email
このコマンドで生成されたファイルに以下のように記述します。
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
def change
add_index :users, :email, unique: true
end
end
unique: trueで一意性がある必要を定義します。
また、users.ymlというファイルの中身は削除します。
#↓全て削除します↓
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString
このインデックスを作成する利点に検索のしやすさがあるらしいのですが、それについてはまた今度にします。
とりあえずこれでメールアドレスの一意性を保つことができるようになるそうなので、備忘録として残しておきます。