[Rails / RSpec] Write Model test (using Shoulda Matchers / FactoryBot)

Introduction

I learned while actually writing RSpec from a state where I had never written a test. I would like to leave what I understand in the article along with the code that actually describes it.

Premise

--Users table --Columns: ```email: stringand password: stringonly --One-to-many association with Books table --Use Gem FactoryBotto generate test data --Use easy-to-write Gem Shoulda Matchers`` for testing

With Shoulda Matchers, you can write a test in one line that you couldn't do without writing many lines. There are some parts that you do not know what is being done, but there are useful syntaxes other than the contents described this time, so if you are interested, I recommend you to check it from the reference site. ..

Reference site

-[Introduction to RSpec that can be used, Part 1 "Understanding the basic syntax and useful functions of RSpec"](https://qiita.com/jnchito/items/42193d066bd61c740612#let-%E3%81%AE%E3% 83% A1% E3% 83% AA% E3% 83% 83% E3% 83% 88% E3% 82% 92% E6% B4% BB% E3% 81% 8B% E3% 81% 97% E3% 81% A6-age-% E3% 82% 82-let-% E3% 81% A7% E7% BD% AE% E3% 81% 8D% E6% 8F% 9B% E3% 81% 88% E3% 82% 8B) -Introduction to RSpec that can be used, part 2 "Mastering frequently used matchers"

It was very easy to understand and was very helpful! Thank you very much!

Model

app/models/user.rb


class User < ApplicationRecord
  has_many :books #One-to-many association with books
  has_secure_password
  before_save { email.downcase! }

  validates :email, presence: true,
                    length: { maximum: 255 },
                    format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i },
                    uniqueness: { case_sensitive: false }
  validates :password, presence: true,
                       length: { minimum: 6 }
end

test

We are doing a total of 9 tests

Whole file

spec/models/user_spec.rb


# rails_Read helper file
require 'rails_helper'

#User model test, so User, type: :As a model
RSpec.describe User, type: :model do

  describe 'has_many' do
    # has_many :Testing books
    it { should have_many(:books) }
  end

  describe 'has_secure_password' do
    # has_secure_password test
    it { should have_secure_password }
  end
  
  describe 'validation' do
    context 'email' do
      #Create test data with FactoryBot
      let(:user) { create(:user) }
      let(:upcase_email_user) { build(:user, :upcase_email) }

      # presence:True test
      it { should validate_presence_of(:email) }
      # length: { maximum: 255 }Test of
      it { should validate_length_of(:email).is_at_most(255) }

      # uniqueness: { case_sensitive: false }Test of
      it 'Do not allow duplicate email storage' do
        duplicate_user = user.dup
        duplicate_user.email = user.email.upcase
        expect(duplicate_user).to be_invalid
      end
     
      # format: { with: XXXXX }Test of
      it 'Do not allow emails that do not fit the specified format' do
        invalid_emails = %w[user@foo,com user_at_foo.org [email protected]@bar_baz.com foo@bar+baz.com [email protected]]
        invalid_emails.each do |invalid_email|
          user.email = invalid_email
          expect(user).to be_invalid
        end
      end

      # before_save { email.downcase! }Test of
      it 'downcase!Is working properly' do
        upcase_email_user.save!
        expect(upcase_email_user.email).to eq '[email protected]'
      end
    end

    context 'password' do
      # presence:True test
      it { should validate_presence_of(:password) }
      # length: { minimum: 6 }Test of
      it { should validate_length_of(:password).is_at_least(6) }
    end
  end
end

Explanation of each description

Test declaration

RSpec.describe model name, type :: test format do, and you can test the User model in the block with this description.

RSpec.describe User, type: :model do

Testing has_many and has_secure_password

Validate model association and application of has_secure_password using Shoulda Matchers

# has_many :Testing books
describe 'has_many' do
  it { should have_many(:books) }
end

# has_secure_password test
describe 'has_secure_password' do
  it { should have_secure_password }
end

Separate test description by describe and context

describe 'validation' do
    context 'email' do
      #Write email validation test
    end
    context 'password' do
      #Write password validation test
    end
end

Create test data with FactoryBot

Define the test data created by the factory bot in a dedicated file

spec/factories/users.rb


FactoryBot.define do
  factory :user do
    #Every time data is generated, a serial number is assigned to create a unique value.
    sequence(:email) { |n| "test#{n}@test.com" }
    password { 'password' }

    trait :upcase_email do
      email { '[email protected]' }
    end
  end
end

Test data is created with let based on the above file, and this test data can be used in the ʻit .. do ~~ end`` block in the described block. In the comment out, the result of checking the contents of ʻuser and` ʻupcase_email_user in binding.pry is described.

  let(:user) { create(:user) }
  # user = <User:0x0000560469431138 id: 2610, email: "[email protected]", password_digest: "[FILTERED]", created_at: Tue, 27 Oct 2020 14:03:04 JST +09:00, updated_at: Tue, 27 Oct 2020 14:03:04 JST +09:00>
  let(:upcase_email_user) { build(:user, :upcase_email) }
  # upcase_email_user = <User:0x00005604690c12c8 id: nil, email: "[email protected]", password_digest: "[FILTERED]", created_at: nil, updated_at: nil>

email validation

presence: true length: {maximum: 255} test (Should a Matchers)

Write tests using Shoulda Matchers

  # presence:Test true
  it { should validate_presence_of(:email) }
  # length: { maximum: 255 }Test
  it { should validate_length_of(:email).is_at_most(255) }

Uniqueness: {case_sensitive: false} test

Create copy data of ʻuser`` with `` dup method``. Verify that `` duplicate_user`` with the same ʻemail as the already registered` ʻuser is not saved with let (: user) {create (: user)}. By capitalizing the email with `ʻupcase`` before the test, it is also verified that the email is judged to be the same even if it is capitalized.

be_invalid will pass if it is scratched by validation (if an error occurs)

it 'Do not allow duplicate email storage' do
  duplicate_user = user.dup
  #Contents at this point
  # duplicate_user = <User:0x000055f1e1d71e90 id: nil, email: "[email protected]", password_digest: "[FILTERED]", created_at: nil, updated_at: nil>
  duplicate_user.email = user.email.upcase
  # user.email.Contents after upcase
  # #<User:0x000055f1e1d71e90 id: nil, email: "[email protected]", password_digest: "[FILTERED]", created_at: nil, updated_at: nil>

  expect(duplicate_user).to be_invalid  # duplicate_user.invalid?Pass if is true
end

format: {with: / \ A [\ w + -.] + @ [Az \ d -] + (. [Az \ d -] +) * . [Az] + \ z / i} test

it 'Do not allow emails that do not fit the specified format' do
  #Prepare 6 arrays of character strings that do not match the specified format
  invalid_emails = %w[user@foo,com user_at_foo.org [email protected]@bar_baz.com foo@bar+baz.com [email protected]]
  #Verify by putting an email that does not match the format in the user's email
  invalid_emails.each do |invalid_email|
    user.email = invalid_email
    expect(user).to be_invalid # duplicate_user.invalid?Pass if is true
  end
end

test before_save {email.downcase!}

It is verified that the email [email protected] of ```upcase_email_userbecomes [email protected] after save. `ʻexpect (X) .to eq Y tests" X is equal to Y ".

it 'downcase!Is working properly' do
  # before_save is called just before save, so save!To do
  upcase_email_user.save!
  expect(upcase_email_user.email).to eq '[email protected]'
end

password validation

Presence: true, length: {maximum: 6} test (Should a Matchers)

Write tests using Should a Matchers as well as email

context 'password' do
  # presence:True test
  it { should validate_presence_of(:password) }
  # length: { maximum: 6 }Test of
  it { should validate_length_of(:password).is_at_least(6) }
end

Finally

At the beginning of learning RSpec, I was not familiar with the syntax and had a lot of trouble. However, there are many articles with easy-to-understand explanations, and it will be fun if you get used to it and pass the test as you expected. I wrote an article about model testing this time, but I also learned about request testing, so I hope I can write an article about it in the future.

Recommended Posts

[Rails / RSpec] Write Model test (using Shoulda Matchers / FactoryBot)
[Rails] Test code using Rspec
[RSpec] Unit test (using gem: factory_bot)
[Ruby on Rails] Model test with RSpec
[Rails] Test with RSpec
[Rails] From test preparation to model unit testing [RSpec]
[Rails] Test of star evaluation function using Raty [Rspec]
[RSpec] Let's use FactoryBot [Rails]
[Rails] About Rspec response test
I tried unit testing Rails app using RSpec and FactoryBot
About method matchers used in model unit test code (RSpec)
Image upload using CarrierWave ~ Rspec test ~
RSpec ~ Task model validation test creation
[RSpec on Rails] How to write test code for beginners by beginners
[Rails5] Rspec -Unit test when nesting-
[RSpec] How to write test code
Let's unit test with [rails] Rspec!
[Rails] Unit test code for User model
Introduce RSpec and write unit test code
[Ruby on Rails] View test with RSpec
[Ruby on Rails] Controller test with RSpec
How to write an RSpec controller test
[Ruby On Rails] When a model unit test is performed with RSpec using FactoryBot, an error occurs because the foreign key is not entered.
[Ruby On Rails] Error in test using RSpec MySQL client is not connected
Generate a test account password using Rails SecureRandom.alphanumeric
Introducing Rspec, a Ruby on Rails test framework
Unit test code for a model using RSpec, which has a little peculiarity ~ User registration