LoginSignup
1
0

More than 3 years have passed since last update.

Railsチュートリアル 第6章 ユーザーのモデルを作成する - ユーザーを検証する

Posted at

現状のUserモデルの問題点

name属性とemail属性に、あらゆる文字列を取ることができる

以下は、name属性の値がnilであるレコードがRDB上に存在することを示す例です。現状のUserモデルの実装では、このようなデータも有効とされてしまいます。

>> User.find(4)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
=> #<User id: 4, name: nil, email: "foo@bar.com", created_at: "2019-10-03 11:06:14", updated_at: "2019-10-03 11:06:14">

さらに、email属性の値にもnilを取ることができてしまいます。

>> User.find(4).update_attributes(email: nil)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", nil], ["updated_at", "2019-10-03 11:17:27.810162"], ["id", 4]]
   (0.3ms)  RELEASE SAVEPOINT active_record_1
=> true
>> User.find(4)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
=> #<User id: 4, name: nil, email: nil, created_at: "2019-10-03 11:06:14", updated_at: "2019-10-03 11:17:27">

あるべき姿

ユーザーアカウントを内容とするRDBのレコードの場合、例えば以下のような制約は実装されていてほしいものです。

  • name属性は空であってはならない
  • email属性はメールアドレスのフォーマットに従う書式である
  • email属性は一意の値である必要がある
    • ログイン時のユーザー名として、メールアドレスを用いる予定であるため

検証/バリデーションとは

Railsの文脈において、検証ないしバリデーションという言葉は、「属性値に対して何らかの制約を課すためのActive Recordの機能」を指します。よく使われる制約の一例としては、以下のようなものが挙げられます。

  • 存在性の制約
  • 長さの制約
  • フォーマットの制約
  • 一意性の制約

バリデーション実装を題材とした、テスト駆動開発の基本の習得

バリデーション実装というのは、テスト駆動開発と相性がよい対象といえます。その理由は以下の通りです。

  • 「値に対してルールを適用する」という動作は、テストの成功・失敗を定義しやすいこと
    • 状態に依存しないため、特に困難なくテストを実装できる
  • バリデーションが失敗した場合、結果としてやや複雑な動作が要求されること
    • どのような理由でバリデーションが通らなかったかをユーザーに提示する動作
    • RDBに当該変更を反映しない動作
  • やや複雑な動作であるため、テストなしでは正常動作の立証が困難であること

というわけで、Railsチュートリアルにおいては、バリデーション実装を題材としてテスト駆動開発を行っています。

具体的には、以下のような手法で開発を進めていくということです。

  1. 有効なモデルであれば通るようなテストを書く
  2. 有効なモデルのオブジェクトを作成する
    • この時点ではテストは通るはず
  3. 作成したオブジェクトの属性のうち、1つを有効でない属性に意図的に変更する
  4. バリデーションで失敗するかどうかをテストする
    • この時点でテストが失敗する

テストコードの実装

モデルに対するテストのモックは、rails generate modelコマンドの実行結果として生成されます。例えば、現在題材としているUserモデルに対しては、以下のモックが既に存在します。

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

見ての通り、現時点では単なるモックです。この時点で重要なのは、「Railsにおけるモデルに対するテストクラスは、ActiveSupport::TestCaseというクラスを継承している」という点でしょうか。コントローラに対するテストがActionDispatch::IntegrationTestというクラスを継承していたのとは違いますね。

ここから実際のテストを書いていきましょう。まずは、有効なUserかどうかをテストするコードです。

est/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end
end

テストメソッドにおいてsetupというメソッドが使われています。Railsのテストフレームワークにおいて、「各テストが走る直前に実行される処理」を記述するためのメソッドです。@userはインスタンス変数ですが、setupメソッド内で宣言しておけば、すべてのテスト内で当該インスタンス変数が使えるようになります。

というわけで、今書いたテストコードによって、valid?メソッドを使ってUserオブジェクトの有効性をテストできる、というわけです。

初めてのテストコード実行

モデルオブジェクトに対するテストを実行するには、開発環境でrails test:modelsというコマンドを実行します。

# rails test:models
Started with run options --seed 19540

  1/1: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.08566s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

余談 - 誤ってrails test:modelというコマンドを実行してしまうとどうなるか

誤ってrails test:modelというコマンドを実行してしまうと、以下のようにスタックトレースを含む長いエラーメッセージが表示されます。

# rails test:model
rails aborted!
Don't know how to build task 'test:model' (See the list of available tasks with `rails --tasks`)
Did you mean?  test:models
/usr/local/bundle/gems/railties-5.1.6/lib/rails/commands/rake/rake_command.rb:21:in `block in perform'
/usr/local/bundle/gems/railties-5.1.6/lib/rails/commands/rake/rake_command.rb:18:in `perform'
/usr/local/bundle/gems/railties-5.1.6/lib/rails/command.rb:46:in `invoke'
/usr/local/bundle/gems/railties-5.1.6/lib/rails/commands.rb:16:in `<top (required)>'
/var/www/sample_app/bin/rails:9:in `require'
/var/www/sample_app/bin/rails:9:in `<top (required)>'
/usr/local/bundle/gems/spring-2.0.2/lib/spring/client/rails.rb:28:in `load'
/usr/local/bundle/gems/spring-2.0.2/lib/spring/client/rails.rb:28:in `call'
/usr/local/bundle/gems/spring-2.0.2/lib/spring/client/command.rb:7:in `call'
/usr/local/bundle/gems/spring-2.0.2/lib/spring/client.rb:30:in `run'
/usr/local/bundle/gems/spring-2.0.2/bin/spring:49:in `<top (required)>'
/usr/local/bundle/gems/spring-2.0.2/lib/spring/binstub.rb:31:in `load'
/usr/local/bundle/gems/spring-2.0.2/lib/spring/binstub.rb:31:in `<top (required)>'
/var/www/sample_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
(See full trace by running task with --trace)

演習 - バリデーション実装を題材とした、テスト駆動開発の基本の習得

1. コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。

>> User.new.valid?
=> true

2. 6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。

>> user.valid?
=> true

存在性を検証する

存在性の検証とは、「渡された属性が存在することを検証する」ことを言います。この節では、以下の存在性検証を実装します。

  • ユーザー情報がRDBに保存される前に、nameフィールドが存在することを検証する
  • ユーザー情報がRDBに保存される前に、emailフィールドが存在することを検証する

name属性の存在性に関するテスト

name属性の存在性に関するテストの追加

test/models/user_test.rbに、まずはname属性の存在性に関するテストを追加します。

`test/models/user_test.rb`
  require 'test_helper'

  class UserTest < ActiveSupport::TestCase

    def setup
      @user = User.new(name: "Example User", email: "user@example.com")
    end

    test "should be valid" do
      assert @user.valid?
    end
+
+   test "name should be present" do
+     @user.name = "    "
+     assert_not @user.valid?
+   end
  end

この時点では、テストが失敗するようになります。

# rails test:models
Started with run options --seed 50708

 FAIL["test_name_should_be_present", UserTest, 0.11231679999036714]
 test_name_should_be_present#UserTest (0.11s)
        Expected true to be nil or false
        test/models/user_test.rb:15:in `block in <class:UserTest>'

  2/2: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.13430s
2 tests, 2 assertions, 1 failures, 0 errors, 0 skips

name属性の存在を検査するには

モデルオブジェクトのクラス定義において、第1引数を:nameとしたvalidatesメソッドを、{ presence: true }というオプションハッシュを第2引数として与えて用いることにより実現できます。

app/models/user.rb
  class User < ApplicationRecord
+   validates :name, presence: true
  end

以下の3つのコードは、いずれも同じ意味です。

class User < ApplicationRecord
  validates :name, presence: true
end
class User < ApplicationRecord
  validates(:name, presence: true)
end
class User < ApplicationRecord
  validates(:name, { presence: true })
end

重要なのは以下の事柄です。

  • Rubyにおいては、特別な場合を除き、メソッド呼び出しの()は省略できる
  • 最後の引数がハッシュである場合、当該ハッシュに対する{}は省略できる

Userモデルに検証を追加した結果を確認してみる

コンソールを起動して、Userモデルに検証を追加した結果を見てみます。

>> user = User.new(name: "", email: "mhartl@example.com")
=> #<User id: nil, name: "", email: "mhartl@example.com", created_at: nil, updated_at: nil>
>> user.valid?
=> false

valid?メソッドの戻り値がfalseになっていますね。

どの検証が失敗したかを確認するには、検証が失敗した際に生成されるerrorsメソッドを使うと便利です。

>> user.errors.full_messages
=> ["Name can't be blank"]
>> user.name.blank?
=> true

エラーメッセージにblankとあるので、blank?メソッドで確認してみました。確かにblank?の結果がtrueになっていますね。

validでないモデルオブジェクトは、RDBに保存できない

valid?falseであるようなモデルオブジェクトは、RDBに保存することができません。

>> user.save
   (1.6ms)  SAVEPOINT active_record_1
   (0.2ms)  ROLLBACK TO SAVEPOINT active_record_1
=> false

ROLLBACKされていますね。

今度こそテストは成功

ここまで確認したところで、改めてrails test:modelsを実行してみます。

# rails test:models
Started with run options --seed 17899

  2/2: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.11375s
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

今度こそテストは成功しました。

email属性の存在性に関するテスト

test/models/user_test.rb
  require 'test_helper'

  class UserTest < ActiveSupport::TestCase

    def setup
      @user = User.new(name: "Example User", email: "user@example.com")
    end

    test "should be valid" do
      assert @user.valid?
    end

    test "name should be present" do
      @user.name = "    "
      assert_not @user.valid?
    end

+   test "email should be present" do
+     @user.email = "    "
+     assert_not @user.valid?
+   end
  end

ここまでの変更に加え、email属性の有効性に対するテストを追加するためには、まずtest/models/user_test.rbに対して上記の変更を行います。

この時点では、以下の通りテストは通りません。

# rails test:models
Started with run options --seed 4227

 FAIL["test_email_should_be_present", UserTest, 0.10753690000274219]
 test_email_should_be_present#UserTest (0.11s)
        Expected true to be nil or false
        test/models/user_test.rb:20:in `block in <class:UserTest>'

  3/3: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.12862s
3 tests, 3 assertions, 1 failures, 0 errors, 0 skips

現時点でテストが通るようにするためには、app/models/user.rbに以下の変更を加えます。

app/models/user.rb
  class User < ApplicationRecord
    validates :name, presence: true
+   validates :email, presence: true
  end

改めてテストを実行します。

# rails test:models
Started with run options --seed 54749

  3/3: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.10906s
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

今度こそテストが通りました。

改めて全体のテストを実行

全てのテストを実行してみます。

# rails test       
Running via Spring preloader in process 2372
Started with run options --seed 40322

  11/11: [=================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.63199s
11 tests, 22 assertions, 0 failures, 0 errors, 0 skips

通りました。

演習 - 存在性の検証

モデルオブジェクト(app/models)に変更を加えた場合、Railsコンソールを再起動しなければ、モデルオブジェクトに対する変更が反映されません。

1.1. 新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。

>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.valid?
=> false

1.2. なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。

>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]

こういうときに使うのは、errors.full_messagesメソッドですね。結果、以下2つの文字列から成る配列を返してきました。

  • Name can't be blank
  • Email can't be blank

2.1. u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。

>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}

2.2. emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?

errors.messagesはハッシュを返すのでしたね。ハッシュのfindメソッドを使います。keyが:emailである要素をfindするのです。

>> u.errors.messages.find {|k,v| k == :email}
=> [:email, ["can't be blank"]]

私事ですが、ハッシュのfindメソッドを、および、ハッシュに関するブロックの構文を覚えていませんでした。

長さを検証する

今度はユーザー名・メールアドレスの長さに制限を与えていきます。

ユーザー名の長さに制限を与える理由として、Railsチュートリアルでは、「ユーザーの名前はサンプルWebサイトに表示される」旨を挙げていました。一方、メールアドレスの長さに制限を与える理由としては、「ほとんどのRDBMSでは文字列の上限を255文字としている」旨を挙げていました。

というわけで、文字列の長さの制限は以下とします。

  • Name属性…最長50文字
  • Email属性…最長255文字

長さに関するテストを追加する

というわけで、以下のコードをtest/models/user_test.rbに追加していきます。

test/models/user_test.rb
  require 'test_helper'

  class UserTest < ActiveSupport::TestCase

  ...略
+
+   test "name should not be too long" do
+     @user.name = "a" * 51
+     assert_not @user.valid?
+   end
+
+   test "email should be not to long" do
+     @user.email = "a" * 244 + "@example.com"
+     assert_not @user.valid?
+   end
  end

長い文字列を作る方法

Rubyでは、文字列のかけ算によって長い文字列を容易に生成することができます。

>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51

上記は、51文字の文字列を生成した例です。今回の学習では、Name属性のバリデーションに用いています。

>> "a" * 244 + "@example.com"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com"
>> ("a" * 244 + "@example.com").length
=> 256

上記は、メールアドレスとして正しい書式になるような256文字の文字列を生成した例です。今回の学習では、Email属性のバリデーションに用いています。

テストは失敗する

# rails test:models
Started with run options --seed 56934

 FAIL["test_name_should_not_be_too_long", UserTest, 0.11051520000910386]
 test_name_should_not_be_too_long#UserTest (0.11s)
        Expected true to be nil or false
        test/models/user_test.rb:25:in `block in <class:UserTest>'

 FAIL["test_email_should_be_not_to_long", UserTest, 0.11830520001240075]
 test_email_should_be_not_to_long#UserTest (0.12s)
        Expected true to be nil or false
        test/models/user_test.rb:30:in `block in <class:UserTest>'

  5/5: [===================================] 100% Time: 00:00:00, Time: 00:00:00

現状でrails test:modelsは失敗します。各FAILメッセージの冒頭には、今追加したテストの名前をスネークケースにしたものが来ています。新たに追加したテストが、確かに正しく動作していますね。

長さを強制するためのコードをモデルに追加する

Railsのモデルクラスのvalidatesメソッドにおいて、最終引数のオプションハッシュで、:lengthというキーを持つ属性が利用できます。:length属性は、引数としてハッシュを取ります。:length属性の引数たるハッシュに:maximumというキーを持つ属性を与えると、:maximum属性の値により、モデルオブジェクトの対応する属性の最大長を決めることができます。

早速、Userモデルのコードを書き換えていきましょう。

app/models/user.rb
  class User < ApplicationRecord
-   validates :name,  presence: true
+   validates :name,  presence: true, length: { maximum: 50 }
-   validates :email, presence: true
+   validates :email, presence: true, length: { maximum: 255 }
  end

上記が「Name属性の最大長50文字・Email属性の最大長255文字という制約をUserクラスに追加する」というコードの例です。

テストが成功した!

Userモデルのコードを書き換えたところで、再びrails test:modelsを実行してみましょう。

# rails test:models
Started with run options --seed 55077

  5/5: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.11015s
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips

今度こそテストが成功しました。

演習 - 長さの検証

1. 長すぎるnameemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。

>> u = User.new(name: "#{("a".."z").to_a.join * 2}", email: "#{("a".."z").to_a.join * 10}@example.net")
=> #<User id: nil, name: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", email: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", created_at: nil, updated_at: nil>
>> u.name.length 
=> 52
>> u.email.length
=> 272
>> u.valid?
=> false

上記は「name属性もemail属性も長すぎる」という例です。valid?は確かにfalseを返しています。

>> u = User.new(name: "#{("a".."z").to_a.join * 2}", email: "#{("a".."z").to_a.join}@example.net")
=> #<User id: nil, name: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", email: "abcdefghijklmnopqrstuvwxyz@example.net", created_at: nil, updated_at: nil>
>> u.name.length
=> 52
>> u.email.length
=> 38
>> u.valid?
=> false

上記、今度は「email属性の長さは適切だが、name属性が長すぎる」という例です。valid?は確かにfalseを返しています。

>> u = User.new(name: "#{("a".."z").to_a.join}", email: "#{("a".."z").to_a.join * 10}@example.net")
=> #<User id: nil, name: "abcdefghijklmnopqrstuvwxyz", email: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", created_at: nil, updated_at: nil>
>> u.name.length
=> 26
>> u.email.length
=> 272
>> u.valid?
=> false

上記、今度は「name属性の長さは適切だが、email属性が長すぎる」という例です。valid?は確かにfalseを返しています。

>> u = User.new(name: "#{("a".."z").to_a.join}", email: "#{("a".."z").to_a.join * 2}@example.net")
=> #<User id: nil, name: "abcdefghijklmnopqrstuvwxyz", email: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", created_at: nil, updated_at: nil>
>> u.name.length
=> 26
>> u.email.length
=> 64
>> u.valid?
=> true

上記、最後は「name属性もemail属性も適切な長さである」という例です。valid?trueになりました。

余談…"#{("a".."z").to_a.join}"の意味

"abcdefghijklmnopqrstuvwxyz"という26文字の文字列を得るためのコードです。実際に行われている処理は以下のとおりです。

  1. "a"から始まり、"z"で終わる範囲を生成する
  2. 1.で生成した範囲を配列に変換する
  3. 2.で生成した配列を文字列に変換する
>> ('a'..'z').to_a.join
=> "abcdefghijklmnopqrstuvwxyz"
>> ('a'..'z').to_a.join.length
=> 26

この文字列に* 10という演算を行えば、260文字の文字列を生成することができます。見事に255文字を超えてきますね。

>> (('a'..'z').to_a.join * 10)       
=> "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
>> (('a'..'z').to_a.join * 10).length
=> 260

2. 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。

>> u = User.new(name: "#{("a".."z").to_a.join * 2}", email: "#{("a".."z").to_a.join * 10}@example.net")
=> #<User id: nil, name: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", email: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx...", created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]
  • Name is too long (maximum is 50 characters)
  • Email is too long (maximum is 255 characters)

以上のエラーメッセージが確認できます。そういえば、errorsオブジェクトの値は副作用により与えられるのですね。

フォーマットを検証する

メールアドレスの形式には、やや複雑なルールが存在します。そのため、単に文字数を制限するだけでは、メールアドレスのバリデーションとしては不十分です。というわけで、「メールアドレスとして最低限の体裁を整えていなければならない」という条件を実装しましょう。

文字列の配列を作る%w[]書式

>> %w[foo bar baz]
=> ["foo", "bar", "baz"]

以下の例は、eachメソッドを使って、addresses配列の各要素を繰り返し取り出す例です。

>> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]

>> addresses.each do |address|
?>   puts address
>> end
USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]

テストを記述する

有効なメールアドレスに対するテスト

test/models/user_test.rb
  class UserTest < ActiveSupport::TestCase
  ...略
+   test "email validation should be 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
  end

assertの第2引数にエラーメッセージを追加していることがポイントです。このようにすれば、どのメールアドレスでテストが失敗したかを特定できます。

assert @user.valid?, "#{valid_address.inspect} should be valid"

inspectメソッドというのは、第4章で出てきたメソッドですね。今回は、詳細な文字列情報を得るために使っています。

このテストを追加した時点では、テストは問題なく通ります。

# rails test:models
Started with run options --seed 22570

  6/6: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.10920s
6 tests, 10 assertions, 0 failures, 0 errors, 0 skips

一つ前のテスト追加の時点では5 tests, 5 assertionsだったのが、今回は6 tests, 10 assertionsになっています。テスト対象のメールアドレスとして、有効なメールアドレス5つから成る配列を与えたからですね。

無効なメールアドレスに対するテスト

test/models/user_test.rb
  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
  end

この時点では、テストが失敗するようになります。

# rails test:models
Started with run options --seed 33633

 FAIL["test_email_validation_should_reject_invalid_addresses", UserTest, 0.079405600001337]
 test_email_validation_should_reject_invalid_addresses#UserTest (0.08s)
        "user@example,com" should be invalid
        test/models/user_test.rb:45:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:43:in `each'
        test/models/user_test.rb:43:in `block in <class:UserTest>'

  7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.13012s
7 tests, 11 assertions, 1 failures, 0 errors, 0 skips

有効なメールアドレスであるかどうかを判別する、実用的な正規表現

Railsチュートリアルでは、「有効なメールアドレスであるかどうかを判別する実用的な正規表現」として、以下のものが挙げられていました。

\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z

この正規表現の意味については、Railsチュートリアル本文内表6.1にまとめられています。

上述正規表現の問題点

ただし、この正規表現を有効なメールアドレスかどうかの判別に使うことは、日本においては若干の問題があります。以下のような「有効でないにもかかわらず、未だ日本国内で使われている可能性があるメールアドレス」が有効であると判定されてしまうためです。

  • .user@example.com(メールアドレスの先頭に.がある)
  • user.@example.com@の直前に.がある)
  • foo..bar@example.com@より前で.が連続している)

これらの有効でないメールアドレスは、2009年3月以前のNTTドコモやauにおいて、キャリアメールのメールアドレスとして作成することができてしまっていたものです。

スクリーンショット 2019-10-09 7.42.43.png

上記スクリーンショットのとおり、Rubularで試した場合も上述した有効でないメールアドレスの例が有効であると判定されています。

メールアドレスの有効性判定をUserモデルに追加する

上述有効でないメールアドレスの問題はさておき、正規表現によるメールアドレスの有効性判定をUserモデルに実装してみましょう。

app/models/user.rb
  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 }   
+   validates :email, presence: true, length: { maximum: 255 },
+                     format: { with: VALID_EMAIL_REGEX }
  end

これでテストが通るようになります。

# rails test:models
Started with run options --seed 38765

  7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.13943s
7 tests, 15 assertions, 0 failures, 0 errors, 0 skips

assertionsの数が11から15に増えました。いいですねぇ。

演習 - フォーマットの検証

1. リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

スクリーンショット 2019-10-09 8.05.26.png

2. 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。

スクリーンショット 2019-10-09 8.06.32.png

foo@bar..comのようにドットが連続した無効なメールアドレスを許容する」というのは、上述スクリーンショットのような挙動をいいます。「@以降で.が連続している」というパターンですね。

このようなメールアドレスを無効と判定できるような実装を追加したい…その第一段階としてのテストの追加です。

test/models/user_test.rb
  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 = %w[user@example,com user_at_foo.org user.name@example.foo@bar_baz.com foo@bar+baz.com foo@bar..com]
      invalid_addresses.each do |invalid_address|
        @user.email = invalid_address
        assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
      end
    end
  end

この時点で、テストを走らせてみます。

# rails test:models
Started with run options --seed 27813

 FAIL["test_email_validation_should_reject_invalid_addresses", UserTest, 0.1293631999869831]
 test_email_validation_should_reject_invalid_addresses#UserTest (0.13s)
        "foo@bar..com" should be invalid
        test/models/user_test.rb:45:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:43:in `each'
        test/models/user_test.rb:43:in `block in <class:UserTest>'

  7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.13355s
7 tests, 16 assertions, 1 failures, 0 errors, 0 skips

期待通り(?)、テストは失敗しました。"foo@bar..com" should be invalidというメッセージ内容を見るに、失敗してほしいところできちんと失敗しているようですね。

2.2. 次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。

app/models/user.rbを書き換えていきます。

app/models/user.rb
  class User < ApplicationRecord
    validates :name,  presence: true, length: { maximum: 50 }
-   VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
+   VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    validates :email, presence: true, length: { maximum: 255 },
                      format: { with: VALID_EMAIL_REGEX }
  end

テストします。

# rails test:models
Started with run options --seed 39103

  7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.13441s
7 tests, 16 assertions, 0 failures, 0 errors, 0 skips

テストが通りました。

3. foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

スクリーンショット 2019-10-10 7.44.38.png

一意性を検証する

メールアドレスの一意性についての重要注意

「メールアドレスの一意性」という観点において、Railsチュートリアルには重要な指摘事項が2点あります。

  • 実用上、メールアドレスの大文字小文字は区別されない
    • バリデーションも大文字小文字を区別しない形で実装しなければならない
    • 「文字列型の大文字小文字を区別するかしないか」は、RDBMSの種類によって異なるので、その違いを吸収するメカニズムを実装しなければならない
  • Active Recordには、RDBMSレベルでのデータの一意性を保証する仕組みはない
    • ほぼ同時に同内容のPOSTリクエストを受け取った場合など、重複してはならないカラムに重複が発生するおそれがある

「これらを踏まえた上で、相応の対策が必要となる」ということです。…恐れずに行ってみましょう!

重複するメールアドレスを拒否する(Rails側で)

テストを書く

一意性をテストするためには、「実際にレコードをRDBMSに保存する」という操作が必要になります。ここで使うのは、モデルオブジェクトのsaveメソッドです。テストコードに登場するのは初めてですね。

test/models/user_test.rb
  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
+     @user.save
+     assert_not duplicate_user.valid?
+   end
  end

テストは通らない

この時点でテストは通りません。assert_notの時点で、RDBMS上にduplicate_userと同一内容のmail属性値を持つレコードが存在するのに、duplicate_uservalid?trueになるためです。

Started with run options --seed 45524

 FAIL["test_email_addresses_should_be_unique", UserTest, 0.11071269999956712]
 test_email_addresses_should_be_unique#UserTest (0.11s)
        Expected true to be nil or false
        test/models/user_test.rb:52:in `block in <class:UserTest>'

  8/8: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.21069s
8 tests, 17 assertions, 1 failures, 0 errors, 0 skips

テストを通す

上記テストを通すためには、Userモデルにおけるemailのバリデーションにuniqueness: trueというオプションを追加すればOKです。

app/models/user.rb
  class User < ApplicationRecord
    validates :name,  presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    validates :email, presence: true, length: { maximum: 255 },
-                     format: { with: VALID_EMAIL_REGEX }
+                     format: { with: VALID_EMAIL_REGEX },
+                     uniqueness: true
  end

実は、「メールアドレスは、実用上大文字小文字を区別しない」ことに対応しなければならなかった

前述「メールアドレスの一意性についての重要注意」にあった事柄の一つですね。Railsチュートリアルのテストコードでは、「メールアドレスを大文字に変換して比較する」という操作を追加しています。

test/models/user_test.rb
  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
+     duplicate_user.email = @user.email.upcase
      @user.save
      assert_not duplicate_user.valid?
    end
  end

「メールアドレスを大文字に変換して比較する」というのが何を指すか、Railsコンソールで順を追って確認してみます。

>> user = User.create(name: "Example User", email: "user@example.com")
   (0.1ms)  SAVEPOINT active_record_1
  User Exists (1.2ms)  SELECT  1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "user@example.com"], ["LIMIT", 1]]
  SQL (14.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2019-10-09 23:07:22.293648"], ["updated_at", "2019-10-09 23:07:22.293648"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Example User", email: "user@example.com", created_at: "2019-10-09 23:07:22", updated_at: "2019-10-09 23:07:22">
>> 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?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "USER@EXAMPLE.COM"], ["LIMIT", 1]]
=> true

最後のduplicate_user.valid?trueになっています。前述"email addresses should be unique"テストを通すためには、duplicate_user.valid?falseにならなければなりません。

モデルオブジェクトにおいて、「一意性検証において大文字小文字を区別しない」という実装をするには、:uniqunessオプションの:case_sensitiveオプションの値falseとすればOKです。

app/models/user.rb
  class User < ApplicationRecord
    validates :name,  presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    validates :email, presence: true, length: { maximum: 255 },
                      format: { with: VALID_EMAIL_REGEX }
                      format: { with: VALID_EMAIL_REGEX },
-                     uniqueness: true
+                     uniqueness: { case_sensitive: false }
  end

:uniquenessオプションの値にハッシュのみを与えている」という点は注目に値します。このような書き方をした場合、Railsは:uniqunessオプションをtrueと判断した上で処理を行います。

この時点で、Rails側のテストは通るようになりました。

# rails test:models
Started with run options --seed 11495

  8/8: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.21977s
8 tests, 17 assertions, 0 failures, 0 errors, 0 skips


# rails test       
Running via Spring preloader in process 2521
Started with run options --seed 16633

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

Finished in 2.45354s
16 tests, 36 assertions, 0 failures, 0 errors, 0 skips

RDBMSレベルでメールアドレスの一意性を保証する

現時点で、Rails側でメールアドレスの重複を排除する仕組みは実装できています。しかし、前述「メールアドレスの一意性についての重要注意」で言及したように、RDBMSレベルでメールアドレスの一意性を保証する仕組みは未だ実装されていません。そのため、現時点では「重複するメールアドレスを持つ複数レコードがRDBMSに登録されてしまう」という事態の発生を排除しきれません。Railsチュートリアルでは、「Submitボタンを素早く2回クリックしてしまった場合」を例として、そうした事態が発生しうることについて言及しています。

というわけで、RDBMS側にもメールアドレスの一意性を保証する仕組みを実装する必要があります。具体的には以下の手順を踏みます。

  1. RDBMS上のemailカラムにインデックスを追加する
  2. 追加したインデックスについて、一意性制約を追加する

変更を加えるのはRDBMS側ですが、この操作はRails側から行うことが可能です。「マイグレーションの実装」という操作ですね。ただ、今回はマイグレーションの新規実装ではなく、既存のモデルへの構造の追加なので、新規実装時とは異なる手順を踏む必要があります。

既存のモデルに構造を追加する

既存のモデルに構造を追加する場合、migrationジェネレーターを用いてマイグレーションを直接作成する必要があります。

# rails generate migration add_index_to_users_email
Running via Spring preloader in process 2535
      invoke  active_record
      create    db/migrate/[timestamp]_add_index_to_users_email.rb

db/migrate/{[timestamp]_add_index_to_users_email.rbというファイルが生成されました。これが新たに作成したマイグレーションの実体です。

生成時点のマイグレーションには、特に何も定義されていません。ここにメールアドレスの一意性制約を追加していきます。

  class AddIndexToUsersEmail < ActiveRecord::Migration[5.1]
    def change
+     add_index :users, :email, unique: true
    end
  end

追加したコードのポイントは以下です。

  • usersテーブルのemailカラムにインデックスを追加している
  • インデックスはRails(ActiveRecord::Migration)のadd_indexメソッドにより追加することができる
    • デフォルトでは一意性制約を追加しない
  • オプションとしてunique: trueを指定することにより、一意性制約を追加できる

マイグレーションのコードを変更したら、最後に実際にマイグレートの処理を行います。

# rails db:migrate  
== [timestamp] AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0222s
== [timestamp] AddIndexToUsersEmail: migrated (0.0224s) ====================

無事マイグレートの処理が完了しました。

fixture

Railsにおけるfixtureというのは、テストに際して初期データを投入する機構のことです。モデルオブジェクトに対応して存在します。最初の生成は、rails generate modelにより自動で行われます。

test/fixtures/users.yml
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString

上記は自動生成されたままの状態のfixture定義データです。見ての通り、メールアドレスが一意ではありません。そのため、「RDBMSにメールアドレスの一意性制約を追加した時点で、一意性制約違反によりテストが通らなくなった」というのが現状です。

なお、そもそもfixtureの内容自体が有効でない代物なのですが、fixtureの内容はバリデーションを通らないゆえ、ここまで問題にはなっていませんでした。

現時点およびこの先しばらくはfixtureを使うことはありません。なので、ここでは単にfixture定義ファイルの内容を単純に空にしてしまうという解決策をとります。

test/fixtures/users.yml
- # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
-  one:
-   name: MyString
-   email: MyString
-
- two:
-   name: MyString
-   email: MyString
-

fixtureの中身を空にした時点で、モデルに対するテストは通るようになります。

# rails test:models
Started with run options --seed 158

  8/8: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.20588s
8 tests, 17 assertions, 0 failures, 0 errors, 0 skips

大文字小文字を区別するRDBMSへの対処

インデックスにおいて大文字と小文字を区別するかは、使用するRDBMSによって異なります。

今回のアプリケーションでは、メールアドレスのインデックスにおいて大文字と小文字は区別してほしくありません。例えば、eRAseRmOToRpHAntOM@example.comEraSErMotOrPhaNTom@ExAMPle.comは同一の文字列と解釈してほしいです。そのため、インデックスにおいて大文字と小文字を区別するRDBMSへの対処が必要となります。

今回は、「データベースに保存される直前に、すべての文字列を小文字に変換する」という方法をとります。例えば、"EraSErMotOrPhaNTom@ExAMPle.com"という文字列が渡されたら、保存直前に"erasermotorphantom@example.com"に変換してしまうわけです。

Active Recordのコールバック(callback)メソッドとは

RailsのActive Recordライブラリでは、オブジェクトのライフサイクル中の特定タイミングに実行されるメソッドを定義することができます。例えば、作成時・保存時・更新時・削除時・検索時・検証時・データベースからの読み込み時などがそのタイミングです。こうした特定タイミングに実行されるメソッドを「コールバック」といいます。

ここで「特定タイミング」と言いました。より厳密には、特定タイミングの「前」「後」というのも指定することができます。例えば、作成前・作成後。保存前・保存後…というような指定も可能です。

コールバックメソッドの実装

繰り返しますと、今回実装するのは、「データベースに保存される直前に、email属性のすべての文字列を小文字に変換する」という処理です。「データベースに保存される直前」に実行されるコールバックは、before_saveという名前で定義されています。

では実装していきましょう。対象ファイルはapp/models/user.rbです。

app/models/user.rb
  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\d\-]+)*\.[a-z]+\z/i
    validates :email, presence: true, length: { maximum: 255 },
                      format: { with: VALID_EMAIL_REGEX },
                      uniqueness: { case_sensitive: false }
  end

重要なポイントは以下です。

  • before_saveメソッドに渡しているのはブロックである
  • 「小文字に変換する」という処理は、Stringクラスのdowncaseメソッドで実現している
  • このコードにおけるselfは、現在のユーザーを指す
  • 代入式右辺のselfは省略することができる
    • 一方、代入式左辺のselfは省略することができない

この実装で実現できること

  • RDBMSのemail属性に一意性制約を追加することにより、email属性の値が重複するユーザーの存在をRDBMSレベルで排除できるようになった
  • RDBMSのemail属性にインデックスを追加することにより、検索効率が向上する
    • メールアドレスからユーザーを引く際に全表スキャンをしなくて済むようになった

演習 - 一意性を検証する

1.1. リスト 6.33を参考に、メールアドレスを小文字にするテストをリスト 6.32に追加してみましょう。

ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。

  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
      duplicate_user.email = @user.email.upcase
      @user.save
      assert_not duplicate_user.valid?
    end
+
+   test "email addresses should be saved as lower-case" do
+     mixed_case_email = "Foo@ExAMPle.com"
+     @user.email = mixed_case_email
+     @user.save
+     assert_equal mixed_case_email.downcase, @user.reload.email
+   end
  end

"email addresses should be saved as lower-case"という名前のテストが、設問内容に対応するテストですね。以下の処理を行っています。

  1. 大文字小文字が混ざったメールアドレスの例をmixed_case_emailオブジェクトとして定義する
  2. テストで使うユーザーオブジェクトのEmail属性に、mixed_case_emailを代入する
  3. テストで使うユーザーオブジェクトをRDBMS上に保存する
  4. mixed_case_emailの値を全て小文字にしたものと、改めてRDBMSから読み込んだユーザーオブジェクトのemail属性の値が一致していればテスト成功

1.2. リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトしてredになることを確認してみましょう。

app/models/user.rb
  class User < ApplicationRecord
-   before_save { self.email = email.downcase }
+ #  before_save { self.email = email.downcase }
    validates :name,  presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    validates :email, presence: true, length: { maximum: 255 },
                      format: { with: VALID_EMAIL_REGEX },
                      uniqueness: { case_sensitive: false }
  end

上述のコードは、before_saveの行をコメントアウトした状態です。これでテストを実行してみましょう。

# rails test:models
Started with run options --seed 47663

 FAIL["test_email_addresses_should_be_saved_as_lower-case", UserTest, 0.11753190000308678]
 test_email_addresses_should_be_saved_as_lower-case#UserTest (0.12s)
        Expected: "foo@example.com"
          Actual: "Foo@ExAMPle.com"
        test/models/user_test.rb:60:in `block in <class:UserTest>'

  9/9: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.27076s
9 tests, 18 assertions, 1 failures, 0 errors, 0 skips

それらしいエラーメッセージがありますね。以下の部分です。

 test_email_addresses_should_be_saved_as_lower-case#UserTest (0.12s)
        Expected: "foo@example.com"
          Actual: "Foo@ExAMPle.com"

"foo@example.com"が入ってくるべきところに、"Foo@ExAMPle.com"が入ってきている」という趣旨のエラーメッセージです。

1.3. before_saveの行のコメントアウトを解除するとgreenになることを確認してみましょう。

app/models/user.rb
  class User < ApplicationRecord
- #  before_save { self.email = email.downcase }
+   before_save { self.email = email.downcase }
    validates :name,  presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    validates :email, presence: true, length: { maximum: 255 },
                      format: { with: VALID_EMAIL_REGEX },
                      uniqueness: { case_sensitive: false }
  end

変更内容は上述となります。テストを実行してみましょう。

# rails test:models
Started with run options --seed 6755

  9/9: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.26453s
9 tests, 18 assertions, 0 failures, 0 errors, 0 skips

テストは無事完了しました。

2. テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。

ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。

app/models/user.rb
  class User < ApplicationRecord
-   before_save { self.email = email.downcase }
+   before_save { email.downcase! }
    validates :name,  presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    validates :email, presence: true, length: { maximum: 255 },
                      format: { with: VALID_EMAIL_REGEX },
                      uniqueness: { case_sensitive: false }
  end

モデルのテストから先に実行してみましょう。コマンドはrails test:modelsですね。

# rails test:models
Started with run options --seed 33265

  9/9: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.26898s
9 tests, 18 assertions, 0 failures, 0 errors, 0 skips

無事完了しました。

続いて、テストスイート全体も実行してみましょう。こちらのコマンドはrails testです。

# rails test
Running via Spring preloader in process 2614
Started with run options --seed 40098

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

Finished in 2.36793s
17 tests, 37 assertions, 0 failures, 0 errors, 0 skips

無事完了しました。

1
0
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
0