1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

railsチュートリアル第6章 を検証する

Posted at

###一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。

まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります14 。
そのため、まずは重複したメールアドレスからテストしていきます。
####重複するメールアドレス拒否のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
.
.
.
  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
  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。
dupは、同じ属性を持つデータを複製するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。

テストをパスさせるために、emailのバリデーションにuniqueness: true というオプションを追加します。

###メールアドレスの一意性を検証する

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: true
          #???
end

実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
すなわち、foo@bar.comFOO@BAR.COMFoO@BAr.coMと書いても扱いは同じです。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要になり、実際のテストコードは下のようにしなければなりません。

####大文字小文字を区別しない、一意性のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end




  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    duplicate_user.email = @user.email.upcase
    # 大文字にする
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードではStringのupcaseメソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。

user = User.create(name: "Example User", email: "user@example.com")
   (0.1ms)  begin transaction
   (0.1ms)  SAVEPOINT active_record_1
  User Create (3.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">

>> user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>

>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user.valid?
=> true

現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここではfalse```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: { case_sensitive: false}
          #???
end

ここで、trueをcase_sensitive: falseに置き換えただけであることに注目してください。
Railsはこの場合、:uniquenessをtrueと判断します。

この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。

ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050

  16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips

###一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。

まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります14 。
そのため、まずは重複したメールアドレスからテストしていきます。
####重複するメールアドレス拒否のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
.
.
.
  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
  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。
dupは、同じ属性を持つデータを複製するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。

テストをパスさせるために、emailのバリデーションにuniqueness: true というオプションを追加します。

###メールアドレスの一意性を検証する

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: true
          #???
end

実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
すなわち、foo@bar.comFOO@BAR.COMFoO@BAr.coMと書いても扱いは同じです。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要になり、実際のテストコードは下のようにしなければなりません。

####大文字小文字を区別しない、一意性のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end




  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    duplicate_user.email = @user.email.upcase
    # 大文字にする
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードではStringのupcaseメソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。

user = User.create(name: "Example User", email: "user@example.com")
   (0.1ms)  begin transaction
   (0.1ms)  SAVEPOINT active_record_1
  User Create (3.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">

>> user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>

>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user.valid?
=> true

現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここではfalse```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: { case_sensitive: false}
          #???
end

ここで、trueをcase_sensitive: falseに置き換えただけであることに注目してください。
Railsはこの場合、:uniquenessをtrueと判断します。

この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。

ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050

  16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips

しかし、依然としてここには1つの問題が残っています。
それはActive Recordはデータベースのレベルでは一意性を保証していないという問題です。
具体的なシナリオを使ってその問題を説明します。
同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。

解決策の実装は簡単です。
実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。
具体的にはデータベース上のemailのカラムにインデックス(index)を追加し、そのインデックスが一意であるようにすれば解決します

#####データベースのインデックス
データベースにカラムを作成するとき、そのカラムでレコードを検索する(find)必要があるかどうかを考えることは重要です。
残念なことに、(インデックスなどの機能を持たない)素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順に読み出し、そのemail属性と渡されたメールアドレスを比較するという非効率的な方法しかありません。
emailカラムにインデックスを追加することで、この問題を解決することができます。
データベースのインデックスを理解するためには、本の索引との類似性を考えるとよいでしょう。
しかし索引のある本であれば、"foobar"を含むすべてのページを索引の中から探すだけで済みます。

emailインデックスを追加すると、データモデリングの変更が必要になります。
今回の場合は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーターを使ってマイグレーションを直接作成する必要があります。

$ rails generate migration add_index_to_users_email

ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。
下のように定義を記述する必要があります。

####メールアドレスの一意性を強制するためのマイグレーション

class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true
    # usersテーブルのemailカラムにインデックスを追加
  end
end

上のコードでは、usersテーブルのemailカラムにインデックスを追加するためにadd_indexというRailsのメソッドを使っています。
インデックス自体は一意性を強制しませんが、オプションでunique: trueを指定することで強制できるようになります。

最後に、データベースをマイグレートします。

$ rails db:migrate

上のマイグレーションが失敗した場合は、実行中のサンドボックスのコンソールセッションを終了していることを確認してください。
そのセッションがデータベースを```ロックしてマイグレーションを妨げている可能性があります。)

この時点では、テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります。
つまり、リスト 6.1でユーザー用のfixtureが自動的に生成されていましたが、ここのメールアドレスが一意になっていないことが原因です(リスト 6.30)
(実はこのデータはいずれも有効ではありませんが、fixture内のサンプルデータはバリデーションを通っていなかったので今まで問題にはなっていなかっただけでした)
####Userのデフォルトfixture

# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString

また、このfixtureは第8章になるまで使わない予定なので、今のところはこれらのデータを削除しておき、ユーザー用のfixtureファイルを空にしておきましょう。

#######一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります。
そのため、まずは重複したメールアドレスからテストしていきます。
####重複するメールアドレス拒否のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
.
.
.
  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
  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。
dupは、同じ属性を持つデータを複製するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。

テストをパスさせるために、emailのバリデーションにuniqueness: true というオプションを追加します。

###メールアドレスの一意性を検証する

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: true
          #???
end

実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
すなわち、foo@bar.comFOO@BAR.COMFoO@BAr.coMと書いても扱いは同じです。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要になり、実際のテストコードは下のようにしなければなりません。

####大文字小文字を区別しない、一意性のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end




  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    duplicate_user.email = @user.email.upcase
    # 大文字にする
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードではStringのupcaseメソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。

user = User.create(name: "Example User", email: "user@example.com")
   (0.1ms)  begin transaction
   (0.1ms)  SAVEPOINT active_record_1
  User Create (3.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">

>> user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>

>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user.valid?
=> true

現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここではfalse```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: { case_sensitive: false}
          #???
end

ここで、trueをcase_sensitive: falseに置き換えただけであることに注目してください。
Railsはこの場合、:uniquenessをtrueと判断します。

この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。

ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050

  16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips

###一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。

まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります14 。
そのため、まずは重複したメールアドレスからテストしていきます。
####重複するメールアドレス拒否のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
.
.
.
  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
  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。
dupは、同じ属性を持つデータを複製するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。

テストをパスさせるために、emailのバリデーションにuniqueness: true というオプションを追加します。

###メールアドレスの一意性を検証する

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: true
          #???
end

実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
すなわち、foo@bar.comFOO@BAR.COMFoO@BAr.coMと書いても扱いは同じです。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要になり、実際のテストコードは下のようにしなければなりません。

####大文字小文字を区別しない、一意性のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end




  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    duplicate_user.email = @user.email.upcase
    # 大文字にする
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードではStringのupcaseメソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。

user = User.create(name: "Example User", email: "user@example.com")
   (0.1ms)  begin transaction
   (0.1ms)  SAVEPOINT active_record_1
  User Create (3.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">

>> user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>

>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user.valid?
=> true

現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここではfalse```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: { case_sensitive: false}
          #???
end

ここで、trueをcase_sensitive: falseに置き換えただけであることに注目してください。
Railsはこの場合、:uniquenessをtrueと判断します。

この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。

ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050

  16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips

しかし、依然としてここには1つの問題が残っています。
それはActive Recordはデータベースのレベルでは一意性を保証していないという問題です。
具体的なシナリオを使ってその問題を説明します。
同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。

解決策の実装は簡単です。
実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。
具体的にはデータベース上のemailのカラムにインデックス(index)を追加し、そのインデックスが一意であるようにすれば解決します

#####データベースのインデックス
データベースにカラムを作成するとき、そのカラムでレコードを検索する(find)必要があるかどうかを考えることは重要です。
残念なことに、(インデックスなどの機能を持たない)素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順に読み出し、そのemail属性と渡されたメールアドレスを比較するという非効率的な方法しかありません。
emailカラムにインデックスを追加することで、この問題を解決することができます。
データベースのインデックスを理解するためには、本の索引との類似性を考えるとよいでしょう。
索引のない本では、渡された言葉(例えば、"foobar")が出てくる箇所をすべて見つけるためには、ページを端から順にめくって最後まで探す必要があります(紙バージョンの全表スキャン)。
しかし索引のある本であれば、"foobar"を含むすべてのページを索引の中から探すだけで済みます。

emailインデックスを追加すると、データモデリングの変更が必要になります。
今回の場合は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーターを使ってマイグレーションを直接作成する必要があります。

$ rails generate migration add_index_to_users_email

ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。
下のように定義を記述する必要があります。

####メールアドレスの一意性を強制するためのマイグレーション

class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true
    # usersテーブルのemailカラムにインデックスを追加
  end
end

上のコードでは、usersテーブルのemailカラムにインデックスを追加するためにadd_indexというRailsのメソッドを使っています。
インデックス自体は一意性を強制しませんが、オプションでunique: trueを指定することで強制できるようになります。

最後に、データベースをマイグレートします。

$ rails db:migrate

上のマイグレーションが失敗した場合は、実行中のサンドボックスのコンソールセッションを終了していることを確認してください。
そのセッションがデータベースを```ロックしてマイグレーションを妨げている可能性があります。)

この時点では、テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります。
つまり、リスト 6.1でユーザー用のfixtureが自動的に生成されていましたが、ここのメールアドレスが一意になっていないことが原因です(リスト 6.30)
(実はこのデータはいずれも有効ではありませんが、fixture内のサンプルデータはバリデーションを通っていなかったので今まで問題にはなっていなかっただけでした)
####Userのデフォルトfixture

# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString

また、このfixtureは第8章になるまで使わない予定なので、今のところはこれらのデータを削除しておき、ユーザー用のfixtureファイルを空にしておきましょう(リスト 6.31)。

#######一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。

まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります14 。
そのため、まずは重複したメールアドレスからテストしていきます。
####重複するメールアドレス拒否のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
.
.
.
  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
  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。
dupは、同じ属性を持つデータを複製するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。

テストをパスさせるために、emailのバリデーションにuniqueness: true というオプションを追加します。

###メールアドレスの一意性を検証する

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: true
          #???
end

実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
すなわち、foo@bar.comFOO@BAR.COMFoO@BAr.coMと書いても扱いは同じです。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要になり、実際のテストコードは下のようにしなければなりません。

####大文字小文字を区別しない、一意性のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end




  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    duplicate_user.email = @user.email.upcase
    # 大文字にする
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードではStringのupcaseメソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。

user = User.create(name: "Example User", email: "user@example.com")
   (0.1ms)  begin transaction
   (0.1ms)  SAVEPOINT active_record_1
  User Create (3.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">

>> user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>

>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user.valid?
=> true

現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここではfalse```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: { case_sensitive: false}
          #???
end

ここで、trueをcase_sensitive: falseに置き換えただけであることに注目してください。
Railsはこの場合、:uniquenessをtrueと判断します。

この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。

ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050

  16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips

###一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。

まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります14 。
そのため、まずは重複したメールアドレスからテストしていきます。
####重複するメールアドレス拒否のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
.
.
.
  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
  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。
dupは、同じ属性を持つデータを複製するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。

テストをパスさせるために、emailのバリデーションにuniqueness: true というオプションを追加します。

###メールアドレスの一意性を検証する

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: true
          #???
end

実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
すなわち、foo@bar.comFOO@BAR.COMFoO@BAr.coMと書いても扱いは同じです。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要になり、実際のテストコードは下のようにしなければなりません。

####大文字小文字を区別しない、一意性のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end




  
  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
  
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    duplicate_user.email = @user.email.upcase
    # 大文字にする
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end

上のコードではStringのupcaseメソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。

user = User.create(name: "Example User", email: "user@example.com")
   (0.1ms)  begin transaction
   (0.1ms)  SAVEPOINT active_record_1
  User Create (3.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">

>> user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>

>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user.valid?
=> true

現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここではfalse```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
          uniqueness: { case_sensitive: false}
          #???
end

ここで、trueをcase_sensitive: falseに置き換えただけであることに注目してください。
Railsはこの場合、:uniquenessをtrueと判断します。

この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。

ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050

  16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips

しかし、依然としてここには1つの問題が残っています。
それはActive Recordはデータベースのレベルでは一意性を保証していないという問題です。
具体的なシナリオを使ってその問題を説明します。
同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。

解決策の実装は簡単です。
実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。
具体的にはデータベース上のemailのカラムにインデックス(index)を追加し、そのインデックスが一意であるようにすれば解決します

#####データベースのインデックス
データベースにカラムを作成するとき、そのカラムでレコードを検索する(find)必要があるかどうかを考えることは重要です。
残念なことに、(インデックスなどの機能を持たない)素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順に読み出し、そのemail属性と渡されたメールアドレスを比較するという非効率的な方法しかありません。
emailカラムにインデックスを追加することで、この問題を解決することができます。
データベースのインデックスを理解するためには、本の索引との類似性を考えるとよいでしょう。
索引のない本では、渡された言葉(例えば、"foobar")が出てくる箇所をすべて見つけるためには、ページを端から順にめくって最後まで探す必要があります。
しかし索引のある本であれば、"foobar"を含むすべてのページを索引の中から探すだけで済みます。

emailインデックスを追加すると、データモデリングの変更が必要になります。
今回の場合は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーターを使ってマイグレーションを直接作成する必要があります。

$ rails generate migration add_index_to_users_email

ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。
下のように定義を記述する必要があります。

####メールアドレスの一意性を強制するためのマイグレーション

class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true
    # usersテーブルのemailカラムにインデックスを追加
  end
end

上のコードでは、usersテーブルのemailカラムにインデックスを追加するためにadd_indexというRailsのメソッドを使っています。
インデックス自体は一意性を強制しませんが、オプションでunique: trueを指定することで強制できるようになります。

最後に、データベースをマイグレートします。

$ rails db:migrate

上のマイグレーションが失敗した場合は、実行中のサンドボックスのコンソールセッションを終了していることを確認してください。
そのセッションがデータベースをロックしてマイグレーションを妨げている可能性があります。

この時点では、テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります。
ユーザー用のfixtureが自動的に生成されていましたが、ここのメールアドレスが一意になっていないことが原因です

####Userのデフォルトfixture

# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString

また、このfixtureは第8章になるまで使わない予定なので、今のところはこれらのデータを削除しておき、ユーザー用のfixtureファイルを空にしておきましょう。

####空のfixtureファイル

#空にする

これで1つの問題が解決されましたが、メールアドレスの一意性を保証するためには、もう1つやらなければならないことがあります。
それは、いくつかのデータベースのアダプタが、常に大文字小文字を区別するインデックス を使っているとは限らない問題への対処です。
この問題を避けるために、今回は「データベースに保存される直前にすべての文字列を小文字に変換する」という対策を採ります。
これを実装するためにActive Recordのコールバック(callback)メソッドを利用します。
このメソッドは、ある特定の時点で呼び出されるメソッドです。
今回の場合は、オブジェクトが保存される時点で処理を実行したいので、before_saveというコールバックを使います。
これを使って、ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換します 。
作成したコードを下示します。
####email属性を小文字に変換してメールアドレスの一意性を保証する

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  # データベースに保存する前に処理をする。
  # 入力されたメールアドレスを小文字にする。
  # コールバックの一つ
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
                    #???
end

コードはbefore_saveコールバックのブロックまでパスし、downcaseという文字列メソッドを用いてユーザーのメールアドレスの現在の値を小文字に変換します。

リスト 6.32ではuniqueness制約をtrueに戻していることにご注目ください。
メールアドレスが小文字で統一されれば、大文字小文字を区別するマッチが問題なく動作できるからです。実際、この手法によってデータベースインデックスを適用する際の問題を防止しています。
制約を元に戻すとテストが失敗しますが、前の形に戻すだけで簡単に修正できます。修正結果をリスト 6.33に改めて示します。
####元のメールuniquenessテストに戻す

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
.
.
.

  test "email addresses should be unique" do
    duplicate_user = @user.dup
    #dup データを複製する
    @user.save
    assert_not duplicate_user.valid?
    # 複製されたデータの存在は有効じゃないかを確認
  end
end
self.email = self.email.downcase

Userモデルの中では右式のselfを省略できるので、今回は次のように書きました
(ちなみにこのselfは現在のユーザーを指します)

self.email = email.downcase

palindrome内でreverseメソッドを使っていたときも、同様のケースであったことを思い出してください。
そのときと同様で、左式ではselfを省略することはできません。したがって、

email = email.downcase

と書くとうまく動きません。

これで、先に述べたアリスのシナリオはうまくいくようになります。
データベースは、最初のリクエストに基づいてユーザーのレコードを保存しますが、2度目の保存は一意性の制約に反するので拒否します。
email属性にインデックスを付与したことによって、メールアドレスからユーザーを引くときに全表スキャンを使わずに済むようになった。

###演習
1.リスト 6.34のように、メールアドレスを小文字にするテストをリスト 6.26に追加してみましょう。
ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。
リスト 6.34のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして red になることを、また、コメントアウトを解除すると green になることを確認してみましょう。

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
.
.
.

  test "email addresses should be saved as lower-case" do
    mixed_case_email = "Foo@ExAMPle.CoM"
    @user.email = mixed_case_email
    #入力されるemail
    @user.save
    assert_equal mixed_case_email.downcase, @user.reload.email
   # 小文字にしたemailと元のデータベースと等しいかを確かめる
  end
end
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 7317
Started with run options --seed 16177

  17/17: [============================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.07393s
17 tests, 36 assertions, 0 failures, 0 errors, 0 skips

比べる相手がないからテストは失敗しそう。

ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 7451
Started with run options --seed 60508

ERROR["test_email_addresses_should_be_saved_as_lower-case", #<Minitest::Reporters::Suite:0x00007fc074cfef40 @name="UserTest">, 0.07783683099978589]
 test_email_addresses_should_be_saved_as_lower-case#UserTest (0.08s)
ActiveRecord::RecordNotFound:         ActiveRecord::RecordNotFound: Couldn't find User without an ID
            test/models/user_test.rb:68:in `block in <class:UserTest>'

  17/17: [============================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.59611s
17 tests, 35 assertions, 0 failures, 1 errors, 0 skips

Couldn't find User without an ID
多分予想通りだと思う。

2.テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。
ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります(リスト 6.35)。

class User < ApplicationRecord
  before_save { email.downcase! }
  # データベースに保存する前に処理をする。
  # 入力されたメールアドレスを小文字にする。
  validates :name,  presence: true, length: { maximum: 50 }
  #属性はname,属性の存在を検証、 最大50字まで
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
  #最大255字まで
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
                    #???
end
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 8051
Started with run options --seed 55108

  17/17: [============================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.56079s
17 tests, 36 assertions, 0 failures, 0 errors, 0 skips
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?