【Rails6】 RSpecによるUserのモデル単体テストの実装

#はじめに
サービスの品質を保つために必要不可欠なテストを実施しております。
今回はUserモデル編ということで、今後他のモデルについても実施し記事にしていきたいと思います。

またテストは追加で行って参りますので、今後もUseのモデル単体テストの内容を追加していきます。

#前提
・以下のgemはインストール済み

```Gemfile
  gem 'rspec-rails', '~> 4.0.0'
  gem 'factory_bot_rails'
  gem 'faker'
```

・ActiveStorageを用いてuserモデルとアソシエーションを紐づけています。

・congig/locales/ja.ymlに適切な日本語のエラーが記述されていること。（省略）

・フォロー機能実装済み



#バージョン
rubyのバージョン　ruby-2.6.5
Railsのバージョン Rails:6.0.0
rspec-rails 4.0.0

#実施したテスト
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1052793/b342dd04-e3d9-020b-779e-dbc8b7491a74.png)

#新規登録画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1052793/f5324e91-13f2-9c8d-4ed0-5fbe696c00f3.png)



##usersテーブルのカラムの紹介

```xxxx_devise_create_users.rb

class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string  :email,                null: false, default: ""
      t.string  :encrypted_password,   null: false, default: ""
      t.string  :nickname,             null: false
      t.text    :occupation,           null: false
      t.text    :position,             null: false
      t.date    :birth_day,            null: false
      t.integer :sex_id,               null: false
      t.boolean :admin,                default: false

#〜省略〜
```

##モデル内のバリデーション

```app/models/user.rb

#〜省略〜

  validates :nickname, presence: true
  validates :occupation, presence: true
  validates :position, presence: true
  validates :birth_day, presence: true
  validates :image, presence: true

  with_options presence: true, numericality: { other_than: 1 } do
  validates :sex_id
  end


  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?[\d])[a-z\d]+\z/i.freeze
  validates_format_of :password, with: PASSWORD_REGEX 
  
  VALID_PASSWORD_REGEX = /\A[a-z0-9]+\z/i
  validates :password, format: { with: VALID_PASSWORD_REGEX }

 validates :email, presence: true, uniqueness: { case_sensitive: true },
  length: { maximum: 255 }

#〜省略〜

```

##FactoryBotの内訳

```spec/factories/users.rb
FactoryBot.define do
  factory :user do
    email                           {Faker::Internet.free_email}
    password                        {'abc1234'}
    password_confirmation           {'abc1234'}
    nickname                        {Faker::Name.initials(number: 2)}
    occupation                      {Faker::Company.name}
    position                        {Faker::Company.name}
    image                           {Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/factories/test.jpg')) }
    birth_day                       {Faker::Date.birthday}
    sex_id                          {Faker::Number.between(2, 5)} 
  end
end

```

##テストコードの内容

```spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  before do
    @user = FactoryBot.build(:user)
  end

  describe 'ユーザー新規登録' do
    context '新規登録がうまくいくとき' do
      it "内容に問題ない場合" do
        expect(@user).to be_valid
      end
      it 'emailが255文字以下のユーザーが作成可能' do
        @user.email = 'a' * 245 + '@sample.jp'
        expect(@user).to be_valid
      end
      it 'emailは全て小文字で保存される' do
        @user.email = 'SAMPLE@SAMPLE.JP'
        @user.save!
        expect(@user.reload.email).to eq 'sample@sample.jp'
      end
    end

    context '新規登録がうまくいかないとき' do
      it "nicknameが空だと登録できない" do
        @user.nickname = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Nicknameを入力してください")
      end
      it "emailが空では登録できない" do
        @user.email = ""
        @user.valid?
        expect(@user.errors.full_messages).to include("Eメールを入力してください")
      end
      it "重複したemailが存在する場合登録できない" do
        @user.save
        another_user = FactoryBot.build(:user)
        another_user.email = @user.email
        another_user.valid?
        expect(another_user.errors.full_messages).to include("Eメールはすでに存在します")
      end
      it 'emailが256文字以上のユーザーを許可しない' do
        @user.email = 'a' * 246 + '@sample.jp'
        @user.valid?
        expect(@user.errors).to be_added(:email, :too_long, count: 255)
      end
      it "passwordが空では登録できない" do
        @user.password = ""
        @user.valid?
        expect(@user.errors.full_messages).to include("パスワードを入力してください")
      end
      it "passwordが5文字以下であれば登録できない" do
        @user.password = "00000"
        @user.password_confirmation = "00000"
        @user.valid?
        expect(@user.errors.full_messages).to include("パスワードは6文字以上で入力してください")
      end
      it "passwordが存在してもpassword_confirmationが空では登録できない" do
        @user.password_confirmation = ""
        @user.valid?
        expect(@user.errors.full_messages).to include("パスワード（確認用）とパスワードの入力が一致しません")
      end
      it "passwordが半角英数字混合でなければ登録できない" do
        @user.password = "aaaaaa"
        @user.valid?
        expect(@user.errors.full_messages).to include("パスワードは不正な値です")
      end
      it "passwordが全角であれば登録できない" do
        @user.password = "ああああああ"
        @user.valid?
        expect(@user.errors.full_messages).to include("パスワードは不正な値です")
      end
      it "所属が空であれば登録できない" do
        @user.occupation = ""
        @user.valid?
        expect(@user.errors.full_messages).to include("Occupationを入力してください")
      end
      it "役職が空であれば登録できない" do
        @user.position = ""
        @user.valid?
        expect(@user.errors.full_messages).to include("Positionを入力してください")
      end
      it "画像が空だと登録できない" do
        @user.image = nil
        @user.valid?
        expect(@user.errors.full_messages).to include("Imageを入力してください")
      end
      it "生年月日が空だと登録できない" do
        @user.birth_day = nil
        @user.valid?
        expect(@user.errors.full_messages).to include("Birth dayを入力してください")
      end
      it "性別が入力されていないとき" do
        @user.sex_id = nil
        @user.valid?
        expect(@user.errors.full_messages).to include("Sexを入力してください", "Sexは数値で入力してください")
      end
      it "性別が1を選択されているとき" do
        @user.sex_id = 1
        @user.valid?
        expect(@user.errors.full_messages).to include("Sexは1以外の値にしてください")
      end
     end
   end

  describe 'パスワードの検証' do
    it 'パスワードと確認用パスワードが間違っている場合、無効であること' do
      @user.password = 'password'
      @user.password_confirmation = 'pass'
      @user.valid?
      expect(@user.errors[:password_confirmation]).to include("とパスワードの入力が一致しません")
    end

    it 'パスワードが暗号化されていること' do
      expect(@user.encrypted_password).to_not eq 'password'
    end
  end

  describe 'フォーマットの検証' do
    it 'メールアドレスが正常なフォーマットの場合、有効であること' 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
        expect(@user).to be_valid
      end
    end
  end

  describe 'フォローの検証' do
  it'ユーザーが他のユーザーをフォロー、フォロー解除可能である' do
    tester1   = FactoryBot.create(:user)
    tester2   = FactoryBot.create(:user)
    tester1.follow(tester2.id)
    expect(tester1.following?(tester2)).to eq true
    tester1.unfollow(tester2.id)
    expect(tester1.following?(tester2)).to eq false
  end
  
  it 'フォロー中のユーザーが削除されると、フォローが解消される' do
    tester1   = FactoryBot.create(:user)
    tester2   = FactoryBot.create(:user)
    tester1.follow(tester2.id)
    expect(tester1.following?(tester2)).to eq true
    tester1.destroy
    expect(tester1.following?(tester2)).to eq false
  end
 end
end


```

#補足説明
##FactoryBotについて
FactoryBotとは、インスタンスをまとめることができるGemのことをいいます。インスタンスを箱の中につめて、後で作業のために開けて中身の道具を使うイメージです。工場の風景を思い浮かべるとわかりやすいかもしれないです。

##Fakerについて
FakerとはRubyでダミーデータを取り扱うgemです。

Fakerの記述の意味

```
Faker::[ジャンル].[タイトル]
```
[ジャンル]と[タイトル]については、公式サイトにて定義されているので、適宜選び記述してください。それ以外を選択すると認識エラーとなります。

https://github.com/faker-ruby/faker


##Rack::Test::UploadedFileについて
factoriesディレクトリにtest.jpgを格納する必要があります。
Rack::Test::UploadedFileはファイルをアップロードするテストに用いられるオブジェクトです。

##Faker::Internet.password(min_length: 6)について
min_length: 6で最低6文字の入力の制限をかけています。

password_confirmationにFakerを用いると、自動生成されてしまいます。なので、passwordとすることで、passwordと同じ値となるようにしています。

password_confirmationに'password'と入力すると文字列と認識され、passwordと別の値とみなされます。deviseのデフォルトで設定されているバリデーションに引っ掛かり、エラーが起こります。

##Faker::Internet.passwordを使わず、手動でパスワードのダミーデータを作成する理由について
稀に文字列のみ、あるいは数字のみが生成されることがあるがあるため、パスワードに対し数字・文字列の組み合わせのバリデーション設定をしている場合は不敵となるため。手動でパスワードのダミーデータを作成をお勧めします。

##Faker::Companyについて
occupation（職業）、position （役職）に用いているFaker::Companyは、見合うものがCompanyに近いのかと思い使用しています。特に、名称が入れば問題ないので、今回は使用しています。
例）〜会社


##emailの文字数は256文字まで
プロバイダーの「ドメイン名」は255文字までで、ユーザー名＋ドメインなどを含めた全体の文字数は256文字までと決まっています。

メールアドレスの長さは、インターネットに関する技術の標準を定める団体である「IETF」が決めた「RFC」と呼ばれるルールで決まっています。

```app/models/user.rb
validates :email, presence: true, uniqueness: { case_sensitive: true },
  length: { maximum: 255 }
```

##emailのバリデーションのPASSWORD_REGEXについて
半角英数混合1字以上。freezeは固定するという意味。

ちなみにFaker::Internet.free_emailで、emailを自動生成します。

```app/models/user.rb
  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?[\d])[a-z\d]+\z/i.freeze
  validates_format_of :password, with: PASSWORD_REGEX 
```

##性別のバリデーションについて
numericality: { other_than: 1 }で、1より上の数を選択するよう制限をかけています。

```app/models/user.rb
with_options presence: true, numericality: { other_than: 1 } do
  validates :sex_id
  end
```

性別はActiveHashを用いております。
モデルのファイルの記述からわかるように1より上の数字を選択するように制限をかけています。

```app/models/sex.rb
class Sex < ActiveHash::Base
  self.data = [
    {id: 1, name: '--' },
    {id: 2, name: 'Not known'}, 
    {id: 3, name: 'Female'}, 
    {id: 4, name: 'Male'},
    {id: 5, name: 'Non-binary'}
  ]

  include ActiveHash::Associations
  has_many :users
end
```

したがって2~5までの範囲で選択する必要があるので以下のインスタンスの設定となります。

```spec/factories/user.rb
sex_id {Faker::Number.between(from:2, to: 5) }
```

##AvtiveHashのテストについて
AvtiveHashはモデルで管理している不変の値ですので、データがない可能性もあります。そこで、nilを用いることでデータがないテストが必要となります。

また1を選択すると'--'を選択してしまう異常の場合もあるので、テストを行います。

```spec/models/user_spec.rb
it "性別が入力されていないとき" do
        @user.sex_id = nil
        @user.valid?
        expect(@user.errors.full_messages).to include("Sexを入力してください", "Sexは数値で入力してください")
      end
      it "性別が1を選択されているとき" do
        @user.sex_id = 1
        @user.valid?
        expect(@user.errors.full_messages).to include("Sexは1以外の値にしてください")
      end
```


##Faker::Date.birthdayについて
自動で日付を入力してくれます。
例）'1990-10-10'

##管理者(admin)権限を付与している場合のインスタンス
以下のようにインスタンスを設定します。

```spec/factories/user.rb
admin {Faker::Boolean.boolean}
```

##フォローの検証について
FactoryBot.create(:user)という記述をcreate(:user)としてしまうと、Rspec側が何をcreateするのか分からずエラーとなります。なのでFactoryBotを記述しておきましょう。

ユーザー一覧内に、ユーザー同士でフォローし合える機能を設けているので、お互いに追加、介助ができるテストを行っています。

またユーザーが削除されると。フォローが解消される仕組みとしているので、そのテストも行っています。