Rails モデルのテストコードについて[備忘録]

今回はモデルのテストについて書いていこうと思います。

<h1>この記事でわかること</h1>
<ul>
<li>Railsのモデルをテストする方法について。</li>
<li>ユーザ名、emailに関してテストするべきことについて。</li>
</ul>

<h1>テスト</h1>
<h2>何をテストするか</h2>
今回は、ユーザ名、emailのレコードをデータベースに保存する際に検証する必要がある以下の項目についてのテストを書いていこうと思います。

<ul>
<li>有効性</li>
<li>存在性</li>
<li>無効性</li>
<li>一意性</li>
</ul>

<h2>有効性のテスト</h2>
rails generateコマンドで生成されたファイルに、user.rb, user_test.rbがあります。それぞれ最初は以下のようになっています。

```Ruby:app/models/user.rb
class User < ApplicationRecord
end
```

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

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

user.rbには、どんなレコードが有効であるかを定義していきます。user_test.rbにはテストを書いていきます。

まずは、以下のようなコードを書きます。

```Ruby:test/models/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で作成されたレコードが有効であるかを検証します。


<br>
では、早速有効性についてのテストを書いていきます。ユーザ名とemailのレコードが空だとよくないので、それをテストします。

```Ruby:test/models/user_test.rb
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で行っていないからです。そこで、以下のコードを追加します。

```Ruby:app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
end
```
これによって、nameとemail属性は空でない時に有効になりました。ちなみに、validatesはメソッドで、:nameとpresence:は引数になります。presence:は「空でない」という意味です。それがtrueなので、「：nameが空でない」と定義されます。

<h2>存在性のテスト</h2>
さて、空でないことを確認することができました。次は、それぞれのレコードの長さについて制限を加えていきます。ユーザ名とメールアドレスはwebサイトに表示するので、長さに制限を加える必要があります。

<br>
<br>
今回は、ユーザ名とメールアドレスをそれぞれ51文字未満と256文字未満に制限します。

まずはテストを書きます。

```Ruby:test/models/user_test.rb
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に制限を定義します。

```Ruby:app/models/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の文字数を制限することができました。


<h2>メールアドレスを検証するテスト</h2>
メールアドレスの検証には以下のコードを使います。

```Ruby:test/models/user_test.rb
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[]では以下のような処理が行われています。

```Ruby
> %w[a b v]
=> ["a", "b", "v"]
```

このメソッドによって、入力された内容を配列として出力します。その配列の要素をeachメソッドで取り出して、それぞれについてテストする形になっています。

メールアドレスの認証には正規表現を使用します。

今回使用する正規表現は以下のようになります。

```Ruby
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
```
意味としては、「英数字、プラス、ハイフン、ドットが少なくとも1つ以上が繰り返された後に@が来て、その後に英数字、ハイフン、ドットが少なくとも1つ以上の後にドットが来て、そのあとは英字が少なくとも一つ以上である。」です。

↓詳しくは次を参照ください。
[基本的な正規表現一覧](https://murashun.jp/article/programming/regular-expression.html)

↓正規表現の確認には以下が便利
[Rubular](https://rubular.com/)

この正規表現をバリデーションに追加します。

```Ruby: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 },
                                format: { with: VALID_EMAIL_REGEX }
end
```
formatオプションは正規表現を引数に持ちます。定数VALID_EMAIL_REGEXに正規表現を代入して、引数にしています。

これで、正規表現でメールアドレスを有効なものとそうでないものに分けるテストを実装できました。


<h1>一意性に関するテスト</h1>
一意性をテストするのは、同じメールアドレスが被っていると問題があるからです。（多分)

テストコードは次のようになります。
```Ruby:test/models/user_test.rb
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に追加します。

```Ruby
before_save {self.email = email.downcase}
```
これはbefore_saveメソッドはsaveメソッドが行われる前実行する処理を定義できます。ここでは、saveメソッドのレシーバーのemail属性の文字を全て小文字に変換する処理が書いてあります。これは、メールアドレスは大文字小文字の区別をしないために必要な処理です。データベースに保存する前に全て小文字に変換することで重複を防げます。

また、以下のコードもuser.rbに追加します。

```Ruby
uniqueness: true
```
このコードによって、重複したメールアドレスは作成することができなくなります。
また以下のようなコードもあります。

```Ruby
uniqueness: { case_sensitive: true }
```
こちらのコードは、メールアドレスの大文字小文字を区別して保存できるようにします。railsではこちらのコードでも、uniqueness: trueと同様の処理を行ってくれるので問題なしです。

追加後のコードが以下のようになります。

```Ruby: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]+\z/i
    validates :email, presence: true, length: {maximum: 255},
                            format: { with: VALID_EMAIL_REGEX },
                            uniqueness: { case_sensitive: true }
end
```

<h1>一意性の仕上げ</h1>

最後に一意性検証の仕上げをしていきます。


ここまでコードだと、ユーザーがメールアドレス保存ボタンを高速で2回クリックしてしまったときなどに一意性を保てなくなります。この場合だと、1回目のアドレスは保存するけど、2回目は保存してはいけません。

そこで、データベースにインデックスというテーブルを新たに作成します。ここにメールアドレスを保存します。その際にマイグレーションで一意性を保つように指定します。以下を実行します。

```Bash
rails generate migration add_index_to_users_email
```
このコマンドで生成されたファイルに以下のように記述します。

```Ruby:db/migrate/20221227042550_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true
  end
end
```
 unique: trueで一意性がある必要を定義します。

また、users.ymlというファイルの中身は削除します。

```Ruby:user.yml
#↓全て削除します↓

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

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString
```
このインデックスを作成する利点に検索のしやすさがあるらしいのですが、それについてはまた今度にします。

とりあえずこれでメールアドレスの一意性を保つことができるようになるそうなので、備忘録として残しておきます。