ぺんぎんのRails日記

ぺんぎん。エンジニア経験ゼロ。Rails勉強中。

【RSpec】システムスペック:間違えていたこと【rails】

今回はRSpecの書き方でわかってなかったことをまとめます。

ユーザー新規作成のときに変数を定義していた

開発時のコントローラでは、お決まりのように変数を定義していたので、なんとなく定義して、expect(user).to be_validとしていた。

これだとuserオブジェクトを要件通りに作ったときのバリデーションチェックになってしまう。

新規作成の成功は、①新規作成ページにアクセス②必要項目を入力③登録ボタンのクリック④フラッシュメッセージの表示⑤ページ遷移ができていることが検証できればいいので、オブジェクトを作る必要はなかった。

作成したタスクの表示の検証はオブジェクトの作成要

「作ったものが表示されているか」を見たいので、ここではオブジェクトを作らないといけない。

作成そのものを検証するならfill_inを使って見ていくけど、すでに作られたものの表示を検証しているところがミソ。

フラッシュメッセージの検証

expect(flash[:notice]).to eq 'message'のようにバリデーションメッセージの時のようにやっていた。
これをするにはshoulda-matchersというgemが必要っぽいのはわかっていたが、何か違う気がしていた。

やはり、そんなことしなくてよくて、expect(page)でページの表示を検証してhave_content 'message'を使えばよかった( ^ω^ )

click_buttonとclick_link

見た目がボタンでもHTMLがa要素になっていたらリンク。どっちか確認するのが面倒くさい場合はclick_onが使える。

click_on 'Create Task'

fill_inとselect

手入力ではなく、セレクトボックスから洗濯する場合は、fill_inじゃなくてselectを使う

select 'done', from: status

.toと.not_to

「〜でない」のときは.not_to

expect(page).not_to have_content 'Hello World'

user.passwordではpasswordを取得できない

これは必ず文字列にしないといけない。

【RSpec】システムスペック【Rails】

システムスペックとは

前回の記事で扱ったモデルスペックは単一のモデルの動作をチェックする単体テストだったのに対し、今回のシステムスペックは、プログラム全体の挙動を確認するための統合テストのひとつです。

システムスペックはCapybaraを使ってブラウザの操作をシミュレートすることができるテストです。

Capybaraの準備

Gemfile

gem 'capybara'

Gemfileに書いたらいつも通りbundle installします。

テスト要件の確認

spec/system/user_spec.rb

RSpec.describe 'Users', type: :system do
  describe 'ログイン前' do
    describe 'ユーザー新規登録' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの新規作成が成功する'
      end
      context 'メールアドレスが未入力' do
        it 'ユーザーの新規作成が失敗する'
      end
      context '登録済のメールアドレスを使用' do
        it 'ユーザーの新規作成が失敗する'
      end
    end
 
    describe 'マイページ' do
      context 'ログインしていない状態' do
        it 'マイページへのアクセスが失敗する'
      end
    end
  end
 
  describe 'ログイン後' do
    describe 'ユーザー編集' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの編集が成功する'
      end
      context 'メールアドレスが未入力' do
        it 'ユーザーの編集が失敗する'
      end
      context '登録済のメールアドレスを使用' do
        it 'ユーザーの編集が失敗する'
      end
      context '他ユーザーの編集ページにアクセス' do
        it '編集ページへのアクセスが失敗する'
      end
    end
 
    describe 'マイページ' do
      context 'タスクを作成' do
        it '新規作成したタスクが表示される'
      end
    end
  end
end

Loginメソッドをmodule化する

今回は上記のuser_spec以外にuser_sessions_spec、task_specでもloginメソッドが必要になるため、moduleとして切り出してまとめてしまいます。

rails_helper.rbで設定を変更する

デフォルトでは、23行目あたりに以下コードがコメントアウトされています。 これを有効にするためにコメントアウトを解除します。

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

さらに、moduleとしてこのあと作成するlogin_supportファイルを読み込んで欲しいので、RSpec.configure do |config|以下に下のコードを追加します。

  config.include LoginSupport

②login_support.rbファイルを作る

moduleを記載するファイルとしてspecディレクトリ内にsupport/login_support.rbファイルを作成します。

spec/support/login_macros.rb

module LoginMacros
  def login_as(user)
    visit root_path
    click_link 'Login'
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password'
    click_button 'Login'
  end
end


これでloginメソッドを一括化することができました( ^ω^ )

では、早速このloginメソッドをテストコードに入れ込んでいきましょう。 ログインの処理なので、'ログイン後'の部分に入れていきます!
spec/system/users_spec.rb

RSpec.describe 'Users', type: :system do
...
  describe 'ログイン後' do
    before { login_as(user) }
...   

これで'ログイン後'内でログイン状態を持続させることができました!

要件の中身を書こう!

では、中身を書いていきます。

descrive 'ログイン前' do

まず正常系の'ユーザーの新規作成が成功する'は、こんな構成で書いていきます。

describe 'ユーザー新規登録' do
  context 'フォームの入力値が正常' do
    it 'ユーザーの新規作成が成功する' do
      ユーザー新規作成ページにアクセス
      メールアドレスを入力する
      パスワードを入力する
      確認用パスワードを入力する
      SignUpボタンを押す
      'User was successfully created.'がページに表示される
      現在のパスがログイン画面のパスになっている
    end
  end
end

・「特定のページにアクセスする」ときの検証方法はvisit パス名を使います。

visit new_user_path

・「入力する」ときはfill in '項目', with '入力内容'です。

fill_in 'Email', with: 'email@example.com'
fill_in 'Password', with: 'password'
fill_in 'Password confirmation', with: 'password'

・「ボタンを押す」はclick 'ボタンのラベル'。

click_button 'SignUp'

・「ページに何かが表示されている」かどうかの検証はexpect(page).to have_contentを使います。

expect(page).to have_content 'User was successfully created.'

・期待したページに遷移できたかどうかの検証は、「現在のページが特定のページと同じであるか」という検証になります。current_pathを使っていきます。

expect(current_path).to eq login_path

次に異常系の'メールアドレスが未入力'を書きます。構成は次の通りです。

describe 'ログイン前' do
    describe 'ユーザー新規登録' do
     ...
      context 'メールアドレスが未入力' do
        it 'ユーザーの新規作成が失敗する' do
          visit new_user_path
          メールアドレスの入力が空
          ...
          expect(page).to have_content '1 error prohibited this user from being saved'
          expect(page).to have_content "Email can't be blank"
          expect(current_path).to eq users_path
        end
      end
  end
end

・fill_inで未入力を検証するときはwith以降を''と空にするか、nilを入れます。

fill_in 'Email', with: ''

次は登録済みのメールアドレスを使った時にユーザーの新規作成に失敗することを検証します。

describe 'ログイン前' do
    describe 'ユーザー新規登録' do
     ...
      context '登録済のメールアドレスを使用' do
        it 'ユーザーの新規作成が失敗する' do
          存在するユーザーを定義
          visit new_user_path
          存在するメールアドレスを入力
          ...
          expect(page).to have_content '1 error prohibited this user from being saved'
          expect(page).to have_content 'Email has already been taken'
          expect(current_path).to eq users_path
          フォームに入力した無効なメールアドレスが入っている
        end
      end
    end
end

・存在するユーザーを定義

existed_user = FactoryBot.create(:user)

・存在するメールアドレスを入力 先ほど定義した変数でメールアドレスを入力します。

fill_in 'Email', with: existed_user.email

・フォームに入力した無効なメールアドレスが入っている これは新しいですね。「ページに表示がある」に似ていますが、have_contentではなくフォーム内に値があるかどうかなのでhave_fieldを使います。

expect(page).to have_field 'Email', with: existed_user.email

最後に、ログインしていない状態でマイページにアクセスできないことを検証します。 userを定義した後で、userのマイページにアクセスし、エラーメッセージが表示されてログイン画面にrenderするという流れです。

describe 'ログイン前' do
     ...
    describe 'マイページ' do
      context 'ログインしていない状態' do
        it 'マイページへのアクセスが失敗する' do
          user = FactoryBot.create(:user)
          visit user_path(user)
          expect(page).to have_content('Login required')
          expect(current_path).to eq login_path
        end
      end
    end
  end

describe 'ログイン後' do

次に、ログイン後の検証です。
書き方はログイン前のときと同じような感じですので、説明は割愛しますが、次のコードを見て見ましょう。

  describe 'ログイン後' do
    describe 'ユーザー編集' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの編集が成功する' do
          user = FactoryBot.create(:user)
          visit edit_user_path(user)
          fill_in 'Email', with: 'update@example.com'
          fill_in 'Password', with: 'update_password'
          fill_in 'Password confirmation', with: 'update_password'
          click_button 'Update'
          expect(page).to have_content('User was successfully updated.')
          expect(current_path).to eq user_path(user)
        end
      end

      context 'メールアドレスが未入力' do
        it 'ユーザーの編集が失敗する' do
          user = FactoryBot.create(:user)
          visit edit_user_path(user)
          fill_in 'Email', with: ''
          fill_in 'Password', with: 'password'
          fill_in 'Password confirmation', with: 'password'
          click_button 'Update'
          expect(page).to have_content('1 error prohibited this user from being saved')
          expect(page).to have_content("Email can't be blank")
          expect(current_path).to eq user_path(user)
        end
      end

      context '登録済のメールアドレスを使用' do
        it 'ユーザーの編集が失敗する' do
          user = FactoryBot.create(:user)
          visit edit_user_path(user)
          other_user = FactoryBot.create(:user)
          fill_in 'Email', with: other_user.email
          fill_in 'Password', with: 'password'
          fill_in 'Password confirmation', with: 'password'
          click_button 'Update'
          expect(page).to have_content('1 error prohibited this user from being saved')
          expect(page).to have_content('Email has already been taken')
          expect(current_path).to eq user_path(user)
        end
      end
      context '他ユーザーの編集ページにアクセス' do
        it '編集ページへのアクセスが失敗する' do
          user = FactoryBot.create(:user)
          other_user = FactoryBot.create(:user)
          visit edit_user_path(other_user)
          expect(page).to have_content 'Forbidden access.'
          expect(current_path).to eq user_path(user)
        end
      end
    end
 
    describe 'マイページ' do
      context 'タスクを作成' do
        it '新規作成したタスクが表示される' do
          user = FactoryBot.create(:user)
          FactoryBot.create(:task, title: 'test_title', status: :doing, user: user)
          visit user_path(user)

これを見ると、userという変数の定義を何度も行なっています。
DRYにしましょう!
前回の記事で、beforeをひとつ上の階層で書くと共通化できると説明しましたが、let(:変数名) { 内容 }で共通化することもできます。こんな感じです。

  describe 'ログイン後' do
    let(:user) { FactoryBot.create(:user) }
    describe 'ユーザー編集' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの編集が成功する' do
          visit edit_user_path(user)
          fill_in 'Email', with: 'update@example.com'
          fill_in 'Password', with: 'update_password'
          fill_in 'Password confirmation', with: 'update_password'
          click_button 'Update'
          expect(page).to have_content('User was successfully updated.')
          expect(current_path).to eq user_path(user)
        end
      end

      context 'メールアドレスが未入力' do
        it 'ユーザーの編集が失敗する' do
          visit edit_user_path(user)
          fill_in 'Email', with: ''
          fill_in 'Password', with: 'password'
          fill_in 'Password confirmation', with: 'password'
          click_button 'Update'
          expect(page).to have_content('1 error prohibited this user from being saved')
          expect(page).to have_content("Email can't be blank")
          expect(current_path).to eq user_path(user)
        end
      end

      context '登録済のメールアドレスを使用' do
        it 'ユーザーの編集が失敗する' do
          visit edit_user_path(user)
          other_user = FactoryBot.create(:user)
          fill_in 'Email', with: other_user.email
          fill_in 'Password', with: 'password'
          fill_in 'Password confirmation', with: 'password'
          click_button 'Update'
          expect(page).to have_content('1 error prohibited this user from being saved')
          expect(page).to have_content('Email has already been taken')
          expect(current_path).to eq user_path(user)
        end
      end
      context '他ユーザーの編集ページにアクセス' do
        it '編集ページへのアクセスが失敗する' do
          other_user = FactoryBot.create(:user)
          visit edit_user_path(other_user)
          expect(page).to have_content 'Forbidden access.'
          expect(current_path).to eq user_path(user)
        end
      end
    end
 
    describe 'マイページ' do
      context 'タスクを作成' do
        it '新規作成したタスクが表示される' do
          FactoryBot.create(:task, title: 'test_title', status: :doing, user: user)
          visit user_path(user)
          expect(page).to have_content('You have 1 task.')
          expect(page).to have_content('test_title')
          expect(page).to have_content('doing')
          expect(page).to have_link('Show')
          expect(page).to have_link('Edit')
          expect(page).to have_link('Destroy')
        end
      end
    end
  end

ちなみにログイン前の検証でも同じ変数を使っている部分があったので、さらに上の階層でletを書いてもいいですね!

今回は省略しましたが、こんな感じで同じシステムスペックのtasks_specとuser_sessionsも書いていきましょうー!

【RSpec】beforeとは【rails】

今回はRSpecの中で使われるbeforeについてサクッとまとめます。

beforeの使い方

beforeは英語で前という意味がありますね。 そうです。describeやcontextを検証していく「前に」処理してくれるものです。

たとえば次のようなテストを見てみましょう。

describe 'タスク管理', type: :system do
  describe '一覧表示' do
    context 'ユーザーAがログインしているとき'
      it 'ユーザーAが作成したタスクが表示される' do
        expect(page).to have_content 'タスク1'
      end
    end
  end
end

「ユーザーAがログインしている」を状態は、「it 'ユーザーAが作成したタスクが表示される' do」を検証する前に処理する必要がありますよね。 なので、ここではit〜doよりもひとつ上の階層でbeforeを使って検証します。

describe 'タスク管理', type: :system do
  describe '一覧表示' do
    context 'ユーザーAがログインしているとき'
      before do
        visit login_path
        fill_in 'メールアドレス', with: 'user_a@example.com'
        fill_in 'パスワード', with: 'password'
        click_button 'ログイン'
      end
      it 'ユーザーAが作成したタスクが表示される' do
        expect(page).to have_content 'タスク1'
      end
    end
  end
end

今はcontext 'ユーザーAがログインしているとき'の下にbeforeを書きましたが、全く同じ処理を別のcontext内でも使うような時には、ひとつ上のdescribe階層でbeforeを書けば、複数のcontextで同じ処理を共通化することもできちゃいます( ^ω^ )

done!

【RSpec】モデルスペック【Rails】

今日はRSpecとFactoryBotを使ったテストの書き方をまとめます。

はじめてテストを作るので基本から復習します。

テストデータを作る

今回はFactoryBotを使ってテストデータを作ります。 今まではseed.rbでサンプル用のデータを作っていましたが、FactoryBotを使えばテスト用のテストデータを作ることができます。
spec/factories/tasks.rb

FactoryBot.define do
  factory :task do
    sequence(:title, "title_1")
    content { "content" }
    status { :todo }
    deadline { 1.week.from_now }
    association :user
  end
end

カラム名 { カラムの内容 } という書き方になっていますね。

sequence

sequenceを使うと.nextメソッドが呼び出されて、数字を増やしていくことができます。

書き方は2種類あって、

sequence(:title){|n| "title_#{n}"}

sequence(:title, "title_1")

のように書くことができます。
.nextの挙動はこんな感じ。

> "title_1".next
 => "title_2"
> "title_2".next
 => "title_3"

association

モデルがassociationされている時に使えるメソッド。 通常、task.createとuser.createの二つを書く必要がありますが、association :userのように書いておけば、user.createを省略することができます!

同じようにspec/factories/users.rbにもuserのテストデータを作っておきましょう。

テスト要件を書く

いきなりコードを書いていくのではなく、はじめにテストしたい内容を書きます。

RSpec.describe Task, type: :model do
  describe 'validation' do
    it 'is valid with all attributes' do end
    it 'is invalid without title' do end
    it 'is invalid without status' do end
    it 'is invalid with a duplicate title' do end
    it 'is valid with another title' do end
  end
end

☆ポイント☆ 以下の2つに分けてテストを書きます。 ・問題が起きない場合のテスト ・問題が起きる場合のテスト

describeとit

・describeメソッド RSpec.describe 'テストの対象' do

テストをグループ化して、「何についてのテストなのか」を記述する。ここでは、taskモデルについてテストすることを宣言しています。

・itメソッド It "〜" do テストをexampleという単位にまとめてくれています。 It "should be〜"やIt "is 〜"のように書き、It do ... endの中に、エクスペクテーション(期待値と実際の値の比較)を書きます。

exampleの書き方

では、実際にItの中に何を書いていくかを見ていきます。

It is valid with all attributes

「すべてのカラムがある場合に、有効である」というテストをします。

It is valid with all attributes
  task = FactoryBot.build(:task)
  expect(task).to be_valid
  expect(task.errors).to be_empty
end

・task = FactoryBot.build(:task)
taskオブジェクトを作ります。

・expect()には検証したいものを書きます。 expect.to の場合は「〜であること」を期待するときに、expect.not_toの場合は「〜でないこと」を期待するときに使います。 ・be_validはマッチャと呼ばれるものです。意味はその名の通り「有効である」という意味です。その逆で、be_invalidという「無効である」というマッチャもあります。 ちなみに、マッチャとは「期待値と実際の値を比較して、一致した(もしくは一致しなかった)という結果を返すオブジェクト」のことです。 ・be_emptyというマッチャはその名の通り、「空である」という意味のマッチャです。
・(復習)errors[:attribute]はエラーがあったらエラーの配列を返すメソッド。エラーがない場合は空の配列を返します。

It is invalid without title

「タイトルがない場合に無効」という内容でテストします。

 it 'is invalid without title' do
      task_without_title = build(:task, title: "")
      expect(task_without_title).to be_invalid
      expect(task_without_title.errors[:title]).to eq ["can't be blank"]
    end

オブジェクトを変数に代入するときは、役割がわかる名前にしておくと◎
・eqは「等しい」という意味のマッチャです。

FactoryBotの設定

設定に以下を記述しておくと、FactoryBot.createのFactoryBot部分を省略できるようになります。

spec/rails_helper.rb

config.filter_rails_from_backtrace!
  # arbitrary gems may also be filtered via:
  # config.filter_gems_from_backtrace("gem name")

  config.include FactoryBot::Syntax::Methods
end

こんな感じでtask = build(:task)書けるようになっていい感じですね。

とりあえず、今回はここまでとします! おやすみなさい🐧🌙

【rails】多対多の関連付け【has_many :through】

モデルの関連付けはそもそもあまり得意じゃないのですが、has_many :throughはもっと苦手です。

ちょっとわかったことをまとめてみます。

1対多の関連付け(belongs_to)

今までやっていたのは以下のような1対他の関連付けでした。

user.rb

has_many :posts

post.rb

belongs_to :user

これはわかりやすいですね。
ユーザーは投稿をたくさん持っていて、投稿は1人のユーザーに従属しています。

問題は次です。

多対多の関連付け

まずは例として、大学生の授業登録のような場面を考えてみたいと思います。

生徒は授業をたくさん持っていて、授業は生徒をたくさん抱えています。

student.rb

has_many :lessons, through: :admin

lesson.rb

has_many :students

(classと区別するために「授業」は"lesson"としました)

これで多対多を表現することができましたが、ここでthroughを使って中間テーブルを入れ込む必要があります。

中間テーブルがない場合

中間テーブル?何それ?おいしいの?って感じですが、これがないと無駄なカラムがたくさん作られてしまうことになるようなんです。

生徒が複数の授業を取っていて、授業も複数の生徒がいるテーブルは以下のようになりますが、一つのカラムに複数の値を入れることはできません。

Image from Gyazo

そこで値ごとにカラムを分けてしまうとこんなふうになります。

Image from Gyazo

気づいた人もいると思いますが、これだと生徒が増えるたびにlessonテーブルのカラムは増えていきます(;∀;)
そして英語や体育のように生徒が少ない授業には、無駄なカラムが増えていくことになります。

すごいムダ٩( ᐛ )و

中間テーブルの設定(has_many :through)

はい、ここからが本題です!
このムダを解消してくれるのが中間テーブルだったんです。

今回は授業登録をしたいので、中間テーブルとしてregistration(登録)テーブルを作ります。

Image from Gyazo

student_id: 1の生徒がlesson_id: 1と2の授業を取っていて、student_id: 2の生徒がlesson_id: 1と3の授業を取っていることを、1つのテーブルにまとめることができました。
こうすれば使わないカラムが作られることはありませんね(^^)

それではhas_many :throughを使ってコードを書いてみましょう。
ちなみに、"through"は「〜を経由して」という意味ですね。
中間テーブルを経由したアソシエーションにしていきます。

student.rb

has_many :registrations
has_many :lessons, through: :registrations

registration.rb

belongs_to :student
belongs_to :lesson

lesson.rb

has_many :registrations
has_many :students, through: :registrations

【ポイント】
ここで気をつけておきたいのが、StudentモデルとLessonモデルでそれぞれhas_many :registrationsをつけているところです。
pikawakaさんのサイトでは「おまじない」として書く決まりになっていると書いてあります。

わたしなりに解釈するなら、「生徒は登録手続きを授業ごとにたくさん行い(student has many registrations)、授業は生徒からの登録をたくさん受け付ける(lessons has many registrations)」という意味合いになるので、どちらにもhas_many :registrationsを付ける、というような感じでしょうか。

has_many :throughでできるようになること

registrationsメソッドを使ってstudentが持っているlessonの値を取得することができるようになっています。

student = Student.first

=> #<Student id: 1, name: "Heiji Hattori", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">

student.lessons

=> [#<Lesson id: 5, subject: "Education", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">, #<Lesson id: 2, subject: "Medicine", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">, #<Lesson id: 3, subject: "Creative Arts", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">]>

ということで服部平次くんは授業を3つ取っているようですね( ^ω^ )

逆にstudentsメソッドで、どの生徒がその授業を取っているかを見ることもできます。

lesson = Lesson.first

=> #<Lesson id: 1, subject: "Computer Science", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">


lesson.students

=> [#<Student id: 4, name: "Ginshiro Toyama", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">, #<Student id: 3, name: "Scar Akai", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">, #<Student id: 2, name: "Yukiko Kudo", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">]>

こちらでは、コンピューターサイエンスの授業を取っているのは、遠山さん、赤井さん、工藤のお母ちゃんということがわかりました!

sourceオプション

has_many :throughはsourceオプションをつけることができます。

これは何かというと、中間テーブルを使って取得するカラム(関連付け元)に別名をつけたい時に使うものです。

つまり、さっき上で書いたこれを

has_many :registrations
has_many :lessons, through: :registrations

こんなふうにできます。

has_many :registrations
has_many :registered_lessons, through: :registrations, source: :lesson

どうやらこのsource: :lessonは、中間テーブルregistrationに記述したbelongs_to :lessonを参照してregisterd_lessonsと一致させているようです。

↑の例だとあまり意味がないのですが、たとえば同じモデル内でhas_many :lessonsが重複してしまう時に、区別するために使うといいと思います。

has_many :registrations
has_many :lessons
has_many :registered_lessons, through: :registrations, source: :lesson

ちょっと無理矢理ですが、履修済みのレッスンがhas_many :lessons、登録はしたが未履修のレッスンがhas_many :registered_lessonsといったところでしょうか。

とにかく、sourceオプションは関連元に別名をつけるときに使うものということでした!

参照サイト

railsguides.jp

pikawaka.com

qiita.com

kimuraysp.hatenablog.com

【rails】バリデーションで同じデータが重複しないようにする【uniqueness】

uniqueness

テーブル内で同じデータが重複しないようにするには、uniqueness: trueを使ってユニーク制約をつけます。

たとえば、同じ名前のユーザーが登録できないようにするには、Userモデルで以下のようにバリデーションを設定します。

user.rb

validates :name, uniqueness: true

これでnameカラムに同じ値が入らないように設定できました。

では、同じユーザー名は存在できるけれど、同じユーザー名、且つ、同じメールアドレスのユーザーが重複しないようにするにはどうしたらいいでしょうか?

validates :name, uniqueness: true
validates :email, uniqueness: true

これだと、nameカラムとemailカラムで別々に検証されるため、同じユーザー名も同じメールアドレスも存在できないことになってしまいます。

このように同時に2つのカラムが絡んだユニーク制約をかけたいときは以下のようにします。

scopeオプションで複数カラムにユニーク制約をかける

uniquenessはオプションでscopeを追加して、検証する範囲を指定することができます。

validates :カラム名1, uniqueness: { scope: :カラム名2 }

"scope"とは英語で「範囲」という意味なので、scopeオプションでユニーク制約をつけると、「カラム2の範囲内でカラム1の値が重複しないようにする」という意味になりますね!

前述のように、同じユーザー名且つ、同じメールアドレスが重複しないようにするには、以下のようにします。

user.rb

validates :name, uniqueness: { scope: :email }

これで同じユーザー名かつ同じメールアドレスの値は重複できなくなりました。

マイグレーションにもユニーク制約をつけるのを忘れずに!

わたしはこれを忘れてユニーク制約が動かなかったので、忘れずにこちらもつけましょう!

class CreateUsers < ActiveRecord::Migration[5.2]

add_index :users, [:name, :email], unique: true

これでDBのインデックスにもユニーク制約をつけることができました。

完成です☆

参考サイト

310nae.com

note.com

saveとsave!、destroyとdestroy!の違い

saveやdestroyには、save!やdestroy!などの破壊的メソッドがあります。

ぺんぎんを含め、初学者には、この違いは少しわかりずらいかもしれませんが、わかったところまでまとめてみたいと思います。

saveやdestroyを使う時

saveやdestroyは、処理が失敗すると想定され、失敗時に別の処理を行いたい時に使います。

def create
  @post = Post.new(post_params)
  if @post.save
    redirect_to posts_path
  else
    render :new
  end
end

上記のような新規投稿では、空のフォームが送信された時や、文字数制限に引っかかる時など、バリデーションが通らない時に処理が失敗することが想定されていますね。

そのため、save!ではなくsaveを使います。

save!やdestroy!を使う時

save!やdestroy!は必ず処理が成功すると想定される時に使います。
万が一処理が失敗する時には、例外を発生させ処理を中断させることができます。

def destroy
  @post = Post.find(params[:id])
  @post.destroy!
  redirect_to posts_path
end

通常、投稿の削除は処理が失敗することなく実行されることが想定されるため、destroy!という破壊的メソッドを使います。

例外処理については、理解が曖昧なので、追ってまとめたいと思います。