[RUBY] [1st] RSpec beginners tried to write ModelSpec as a beginner

Introduction

Nice to meet you. First of all, I would like to briefly introduce myself. From May 2020 to September 2020, I am a graduate who studied mainly Ruby at DMM WEB CAMP and is currently changing jobs. The other day, a study session for beginners [(RSpec Beginners !!)](https://www.youtube.com/watch?v=4lgaIeKdrR4 "" was held with the kindness of RSpec male Junichi Ito @jnchito. We also participated in RSpec Beginners !! ").

I felt while writing a test using RSpec in my portfolio, If you are a beginner like me, I have summarized the "stumbling points" such as "I'm going to stumble here: thinking:" and "I don't know here: hugging:" from the perspective of a beginner. Also, as I went along, I sometimes wanted a concrete test code example of the function I wanted to write, so I also serve as a review of myself, but if this article is a little helpful for RSpec beginners I'm happy.

What to do in this article

Not covered in this article

Premise

Testing the individual user model

① Introduction of individual user model

First of all, I will show you the individual user model. Actually, there are associations such as favorite models, but since we will not deal with them this time, we have deliberately deleted them.

①user.rb


class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :rooms, dependent: :destroy
  has_many :messages, dependent: :destroy
  has_many :notifications, dependent: :destroy
  
  validates :last_name, presence: true, length: { maximum: 10 }
  validates :first_name, presence: true, length: { maximum: 10 }
  validates :kana_last_name, presence: true, length: { maximum: 10 }
  validates :kana_first_name, presence: true, length: { maximum: 10 }
  validates :email, presence: true, length: { maximum: 30 }
  validates :postal_code, presence: true, length: { minimum: 7, maximum: 7 }
  validates :address, presence: true, length: { maximum: 30 }
  validates :phone_number, presence: true, length: { maximum: 12 }
  validates :introduction, length: { maximum: 200 }

  attachment :profile_image

  def full_name
    last_name + " " + first_name
  end

  def kana_full_name
    kana_last_name + " " + kana_first_name
  end
end

(2) Use FactoryBot and prepare user data in advance.

** Stumble point: hugging: "What is Factory Bot?" ** FactoryBot is also described in "Everyday Rails-Introduction to Rails Testing with RSpec". You can use it by inserting a gem, so you may try searching with FactoryBot. I didn't know at first, but it's okay. It's just a convenient thing that you can simply describe the test data and assign it to an instance variable or local variable such as @user in ③. How to use is described in ③. First, run $ bin / rails g factory_bot: model user to create a file, so when it is created, try inserting sample data according to the columns of your application.

②spec/factories/users.rb


FactoryBot.define do
  #Use FactoryBot and prepare user data in advance
  factory :user do
    last_name { "test" }
    first_name { "Taro" }
    kana_last_name { "test" }
    kana_first_name { "Taro" }
    email { "[email protected]" }
    postal_code { "1234567" }
    address { "123, Chiyoda-ku, Tokyo-12-1" }
    phone_number { "12345678910" }
    password { "testtaro" }
  end
end

③ Specific test description

I will finally write a test. rest assured. I will stumble properly later. Running $ bin / rails g rspec: model user will create user_spec.rb in the spec / models folder. Model test code (validation, self-created methods, etc.) will be written in this file.

Below is a completed example.

③spec/models/user_spec.rb


require 'rails_helper'

RSpec.describe User, type: :model do

  before do
    @user = FactoryBot.build(:user)
  end

  describe "Validation test" do
    it "Last name, first name, Kana surname, Kana first name, email, zip code, address, phone number, password must be valid" do
      expect(@user).to be_valid
    end

    it "Must be invalid without surname" do
      @user.last_name = ""
      @user.valid?
      expect(@user.errors[:last_name]).to include("Please enter")
    end

    it "Must be invalid unless the surname is 10 characters or less" do
      @user.last_name = "a" * 11
      @user.valid?
      expect(@user.errors[:last_name]).to include("Please enter within 10 characters")
    end

    it "If there is no name, it is in an invalid state" do
      @user.first_name = ""
      @user.valid?
      expect(@user.errors[:first_name]).to include("Please enter")
    end

    it "The name must be 10 characters or less and must be invalid" do
      @user.first_name = "a" * 11
      @user.valid?
      expect(@user.errors[:first_name]).to include("Please enter within 10 characters")
    end
    
    it "Must be invalid without Kana surname" do
      @user.kana_last_name = ""
      @user.valid?
      expect(@user.errors[:kana_last_name]).to include("Please enter")
    end

    it "Kana Last name must be 10 characters or less to be invalid" do
      @user.kana_last_name = "a" * 11
      @user.valid?
      expect(@user.errors[:kana_last_name]).to include("Please enter within 10 characters")
    end

    it "If there is no kana name, it is in an invalid state" do
      @user.kana_first_name = ""
      @user.valid?
      expect(@user.errors[:kana_first_name]).to include("Please enter")
    end

    it "The kana name must be 10 characters or less to be invalid" do
      @user.kana_first_name = "a" * 11
      @user.valid?
      expect(@user.errors[:kana_first_name]).to include("Please enter within 10 characters")
    end
    
    it "If there is no email address, it is invalid" do
      @user.email = ""
      @user.valid?
      expect(@user.errors[:email]).to include("Please enter")
    end

    it "The email address must be 30 characters or less to be invalid" do
      @user.email = "a" * 31
      @user.valid?
      expect(@user.errors[:email]).to include("Please enter 30 characters or less.")
    end
    
    it "Must be invalid without zip code" do
      @user.postal_code = ""
      @user.valid?
      expect(@user.errors[:postal_code]).to include("Please enter")
    end

    it "If the zip code is less than 7 characters, it is invalid" do
      @user.postal_code = "a" * 6
      @user.valid?
      expect(@user.errors[:postal_code]).to include("Please enter at least 7 characters")
    end

    it "If the zip code exceeds 7 characters, it is invalid." do
      @user.postal_code = "a" * 8
      @user.valid?
      expect(@user.errors[:postal_code]).to include("Please enter within 7 characters")
    end
    
    it "Must be invalid without an address" do
      @user.address = ""
      @user.valid?
      expect(@user.errors[:address]).to include("Please enter")
    end

    it "The address must be 30 characters or less to be invalid" do
      @user.address = "a" * 31
      @user.valid?
      expect(@user.errors[:address]).to include("Please enter within 30 characters")
    end
    
    it "If there is no phone number, it is invalid" do
      @user.phone_number = ""
      @user.valid?
      expect(@user.errors[:phone_number]).to include("Please enter")
    end

    it "The phone number must be 12 characters or less to be invalid" do
      @user.phone_number = "a" * 13
      @user.valid?
      expect(@user.errors[:phone_number]).to include("Please enter within 12 characters")
    end

    it "The self-introduction text must be 200 characters or less to be invalid." do
      @user.introduction = "a" * 201
      @user.valid?
      expect(@user.errors[:introduction]).to include("Please enter within 200 characters")
    end

    it "The password must be 6 characters or more and is invalid" do
      @user.password = "a" * 5
      @user.valid?
      expect(@user.errors[:password]).to include("Please enter at least 6 characters.")
    end
    
    it "If the email address is duplicated, it must be invalid." do
      FactoryBot.create(:user)
      @user.valid?
      expect(@user.errors[:email]).to include("Already exists.")
    end
  end

  describe "Instance method testing" do
    it "Returning the user's full name as a string" do
      @user.last_name = "test"
      @user.first_name = "Taro"
      expect(@user.full_name).to eq "Test Taro"
    end

    it "Returning the user's kana full name as a string" do
      @user.kana_last_name = "test"
      @user.kana_first_name = "Taro"
      expect(@user.kana_full_name).to eq "Test Taro"
    end
  end

end

"Hmmmm, I'll write the test code in this file. OK: vulcan: I don't really understand from the first line! ** require'rails_helper' ** What's that: hugging:" Those who thought. This tells RSpec that the Rails application needs to be loaded to run the tests in the file. This description is required for almost every file in the test suite. (Quoted from Everyday Rails) I'm starting to think that I don't need to explain anything about Everyday Rails, but I'll do my best.

In short, by writing require'rails_helper', for RSpec seniors ** "My application looks like this! The model has this data, the controller describes it like this, and the view shows this! Please understand!" ** I am telling you that. Thanks to this description, RSpec will test your application against the code you write.

So what is that rails_helper? Those who thought. Rspec settings can be written in ** rails_helper.rb ** in the spec folder. The setting to enable the devise helper method in system_spec, which will be used in the system specs scheduled to be described next time, is also written here.

The part of this setting etc. is also described in Everyday Rails. I'm really relying on Everyday Rails, but I recommend it because it's a book that covers specific descriptions of test code and why this description is necessary.

spec/rails_helper.rb


#devise helper method system_Make available in spec
  config.include Devise::Test::IntegrationHelpers, type: :system

I'm sure there are people who say, "I understand rails_helper somehow, but I don't know if only the completed code below it is shown: hugging:", so I'll explain it briefly. However, I have written more about testing the user model in Everyday Rails, so it may be better to refer to that.

See the code example below. First, the before block is used to make the code DRY when the same data is required for multiple tests in the same file. In this case, the test data of spec / factories / users.rb described in (2) above is built and assigned to @user by the description of FactoryBot.build (: user) on the right side. By doing so, @user can be used in later it blocks etc. Overwrite the last_name data with empty @ user.last_name = "" on the first line of the it block, such as "If @ user.last_name is nil, it is invalid ~" or @ user.last_name = "a" * In 11, I overwrote the data "aaaaaaaaaaaa" to last_name and wrote a test such as "last_name is validated, so it is invalid unless it is 10 characters or less." By the way, the be_valid in front of it is called Matcher. Regarding matcha, Mr. Ito's [Introduction to RSpec that can be used, part 2 "Mastering frequently used matcha" ](Https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5 "Introduction to RSpec that can be used, part 2" Mastering frequently used matchers " I think you should refer to ").

** Stumble point: hugging: "Factory Bot description position" ** Also, by placing a before block directly under RSpec.describe User, type:: model do and describing @user created by FactoryBot in it, @user in the before block is all in the same file. It can be used in tests. This time it's still a simple association, so it's better, but as the number of associations increases, more and more data is created with FactoryBot, and I'm addicted to the position to describe at that time. Especially System Spec. It was a swamp. I will talk about it later.

③spec/models/user_spec.rb


RSpec.describe User, type: :model do
#The before block is used to make the code DRY when the same data is required for multiple tests in the same file.
 before do
   @user = FactoryBot.build(:user) # @last for user_name{ "test" }、first_name { "Taro" }Contains data such as
 end

 it "Last name, first name, Kana surname, Kana first name, email, zip code, address, phone number, password must be valid" do
   expect(@user).to be_valid
 end

 it "Must be invalid without surname" do
   @user.last_name = "" # @user.last_If the name is nil, it's invalid ~
   @user.valid?
   expect(@user.errors[:last_name]).to include("Please enter")
 end

 it "Must be invalid unless the surname is 10 characters or less" do
   @user.last_name = "a" * 11 # last_Since name is being validated, it is invalid unless it is 10 characters or less ~
   @user.valid?
   expect(@user.errors[:last_name]).to include("Please enter within 10 characters")
 end

Also, only the following tests look a little different. FactoryBot.create (: user) is sandwiched in the first line. In the before block mentioned above, @user is created before the it block. So is this test. However, @user in the before block this time is ** build **, not create, so it will not be saved in the database unless it is saved. In the test below, by writing FactoryBot.create (: user) in the first line, this will be saved before @user in the before block in order. After that, when you ask @ user.valid? (Is @user valid?), The same data is saved first, so you can expect the wording include ("already exists."). Right.

③spec/models/user_spec.rb


it "If the email address is duplicated, it must be invalid." do
  FactoryBot.create(:user) #Saved first
  @user.valid?
  expect(@user.errors[:email]).to include("Already exists.")
end

The rest of the it block has the same structure, and I think it's understandable somehow, so I'll fold it. Please note that the error statement for the include (please enter "") part of expect differs from person to person. If you have translated the error by i18n into Japanese, check it with devise.ja.yml etc., or check the error text of the terminal at runtime, so check each and enter the wording.


Finally, test the method. Mr. Ito also mentioned in the RSpec Beginners video that it is important to test the validation of the model, but it is also important to test the method etc. created by yourself **. Also, it seems that it is not necessary to write about the association test. (However, if the name is duplicated and a pseudo model is created using class_name etc. (follow function etc.), it is okay to do it.) Rails does a good job with associations, so if you do, you should make sure that the methods you create work correctly. Certainly I thought ~ ~ ~. It's mentioned in the video, so please take a look. So it's super easy, but I'll also write a method test below.

③spec/models/user_spec.rb


describe "Instance method testing" do
  it "Returning the user's full name as a string" do
    @user.last_name = "test"
    @user.first_name = "Taro"
    expect(@user.full_name).to eq "Test Taro"
  end

  it "Returning the user's kana full name as a string" do
    @user.kana_last_name = "test"
    @user.kana_first_name = "Taro"
    expect(@user.kana_full_name).to eq "Test Taro"
  end
end

Well, it's easy enough to understand without any explanation, so I won't explain it. It's just that the full_name method and kana_full_name method described in the model of ① are properly full names. As for method testing, I can't write anything complicated, so I'll study it. ..

Testing the corporate user model

Actually, there are associations such as Relationship model, but we will not deal with it this time, so we have deleted it. Since the test for corporate users is almost the same as for individual users, only the code is described, so please refer to the following.

①company.rb


class Company < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :rooms, dependent: :destroy
  has_many :messages, dependent: :destroy
  has_many :articles, dependent: :destroy
  has_many :notifications, dependent: :destroy

  validates :company_name, presence: true, length: { maximum: 30 }
  validates :kana_company_name, presence: true, length: { maximum: 30 }
  validates :email, presence: true, length: { maximum: 30 }
  validates :postal_code, presence: true, length: { minimum: 7, maximum: 7 }
  validates :address, presence: true, length: { maximum: 30 }
  validates :phone_number, presence: true, length: { maximum: 12 }
  validates :introduction, length: { maximum: 800 }

  attachment :profile_image
  attachment :background_image

  #If approved is true, you can log in. At the time of new registration, default is false, so you cannot log in.
  def active_for_authentication?
    super && self.approved?
  end

  #The message after the login is flipped above. For wording details, see config/locales/devise.ja.Described in yml.
  def inactive_message
    self.approved? ? super : :needs_admin_approval
  end

  def followed_by?(user)
    passive_relationships.find_by(following_id: user.id).present?
  end
end

②spec/factories/companies.rb


FactoryBot.define do
  factory :company do
    company_name { "Test Co., Ltd." }
    kana_company_name { "Test Co., Ltd." }
    email { "[email protected]" }
    postal_code { "1234567" }
    address { "123, Chiyoda-ku, Tokyo-12-1" }
    phone_number { "12345678910" }
    password { "testcompany" }
    approved { true }
    is_active { true }
  end
end

③spec/models/company_spec.rb


require 'rails_helper'

RSpec.describe Company, type: :model do

  describe "Validation test" do

    before do
      @company = FactoryBot.build(:company)
    end

    it "If you have the company name, company kana name, email, zip code, address, phone number, and password, they must be valid." do
      expect(@company).to be_valid
    end

    it "It is invalid without the company name" do
      @company.company_name = ""
      @company.valid?
      expect(@company.errors[:company_name]).to include("Please enter")
    end

    it "It is invalid if there is no company kana name" do
      @company.kana_company_name = ""
      @company.valid?
      expect(@company.errors[:kana_company_name]).to include("Please enter")
    end
    
    it "If there is no email address, it is invalid" do
      @company.email = ""
      @company.valid?
      expect(@company.errors[:email]).to include("Please enter")
    end
    
    it "Must be invalid without zip code" do
      @company.postal_code = ""
      @company.valid?
      expect(@company.errors[:postal_code]).to include("Please enter")
    end
    
    it "Must be invalid without an address" do
      @company.address = ""
      @company.valid?
      expect(@company.errors[:address]).to include("Please enter")
    end
    
    it "If there is no phone number, it is invalid" do
      @company.phone_number = ""
      @company.valid?
      expect(@company.errors[:phone_number]).to include("Please enter")
    end

    it "The password must be 6 characters or more and is invalid" do
      @company.password = "a" * 5
      @company.valid?
      expect(@company.errors[:password]).to include("Please enter at least 6 characters")
    end
    
    it "If the email address is duplicated, it must be invalid." do
      FactoryBot.create(:company)
      @company.valid?
      expect(@company.errors[:email]).to include("Already exists")
    end
    
  end
end

Genre model test

The genre is not particularly difficult, so I will omit the explanation.

①genre.rb


class Genre < ApplicationRecord
  has_many :articles, dependent: :destroy
  validates :genre_name, presence: true, length: { maximum: 15 }
end

②spec/factories/genres.rb


FactoryBot.define do
  factory :genre do
    genre_name { "Test genre" }
    is_active { true }
  end
end

③spec/models/genre_spec.rb


require 'rails_helper'

RSpec.describe Genre, type: :model do
  describe "Validation test" do
    it "If there is no genre name, it is invalid" do
      @genre = FactoryBot.build(:genre)
      @genre.genre_name = ""
      @genre.valid?
      expect(@genre.errors[:genre_name]).to include("Please enter")
    end
  end
end

Article model testing

① Introduction of article model

I'm finally here. The model specs in the article are slightly different, so I would like to explain them briefly. Below is the article model.

①article.rb


class Article < ApplicationRecord
  belongs_to :company
  belongs_to :genre

  validates :title, presence: true, length: { maximum: 35 }
  validates :body, presence: true

  attachment :image

  #Search only for articles with valid publication status and genre enabled
  def self.all_active
    where(is_active: true).joins(:genre).where(genres: {is_active: true})
  end

  def favorited_by?(user)
    favorites.where(user_id: user.id).exists?
  end
end

(2) Use FactoryBot and prepare article data in advance.

②spec/factories/articles.rb


FactoryBot.define do
  factory :article do
    title { "Test title" }
    body { "Test body" }
    is_active { true }
    company
    genre
  end
end

③ Specific test description

I will write a test of the article model. $ bin / rails g rspec: model article will create article_spec.rb in the spec / models folder. Below is an example of the completed code.

③spec/models/article_spec.rb


require 'rails_helper'

RSpec.describe Article, type: :model do

  describe "Article test" do
    before do
      @company = FactoryBot.create(:company)
      @genre = FactoryBot.create(:genre)
      @article = FactoryBot.build(:article)
    end

    #article creation
    context "When all data is included" do
      it "All are entered so it will be saved" do
        @article.company_id = @company.id
        @article.genre_id = @genre.id
        expect(@article.save).to be true
      end
    end

    context "When not all data is included" do
      it "Not saved because not all have been entered" do
        @article.company_id = @company.id
        @article.genre_id = @genre.id
        @article.title = ""
        @article.body = ""
        expect(@article.save).to be false
      end
    end
  end

  describe "Validation test" do
    before do
      @article = FactoryBot.build(:article)
    end

    context "If the title does not exist" do
      it "Being in an invalid state" do
        @article.title = ""
        @article.valid?
        expect(@article.errors[:title]).to include("Please enter")
      end
    end

    context "If the title exceeds 35 characters" do
      it "I get an error message" do
        @article.title = "a" * 36
        @article.valid?
        expect(@article.errors[:title]).to include("Please enter within 35 characters")
      end
    end

    it "If there is no text, it is invalid" do
      @article.body = ""
      @article.valid?
      expect(@article.errors[:body]).to include("Please enter")
    end
    
  end
end

Well, there is nothing new so far, but the article model is as described in ① belongs_to :company belongs_to :genre It is an association like that, and it is a little different from before. As shown below, in the before block, company and genre are first created with FactoryBot, saved in the database, and then assigned to @company and @genre, respectively. The flow of article posting There is a company → Select an article genre → You can post an article It has become a flow. Based on this, since ** corporation and genre must exist ** to post an article, the before block is described as follows.

The rest is easy.

  1. Which company posted the article, 2. Which genre of the article Substitute @ article.company_id and @ article.genre_id respectively,
  2. If all the data is included, 2. If all the data is not included, just test separately in context!

③spec/models/article_spec.rb


RSpec.describe Article, type: :model do

  describe "Article test" do
    before do
      #There is a company → Select an article genre → You can post an article
      @company = FactoryBot.create(:company)
      @genre = FactoryBot.create(:genre)
      @article = FactoryBot.build(:article)
    end

    #article creation
    context "When all data is included" do
      it "All are entered so it will be saved" do
        @article.company_id = @company.id
        @article.genre_id = @genre.id
        expect(@article.save).to be true
      end
    end

    context "When not all data is included" do
      it "Not saved because not all have been entered" do
        @article.company_id = @company.id
        @article.genre_id = @genre.id
        @article.title = ""
        @article.body = ""
        expect(@article.save).to be false
      end
    end
  end

This completes the model test! There is also a model spec of the message model, but I will omit it because the number of items is small. If you are interested, please see from GitHub.

At the end

This time, we have summarized the model testing and model specifications, which are the core parts of the application. Model specs are also important, but the most important ones are the system specs described next time. Test the behavior in a real browser, such as a user editing My Page or sending a DM to a corporation. There were many points to stumble. Maybe I'm the only one: thinking: I'd like to summarize it in an easy-to-understand manner, so I hope you'll see it again next time. Thank you for watching until the end.

** 2020.09.28 System specs edition has been posted. Please see here. ** **

Reference article

https://qiita.com/jnchito/items/2a5d3e15761fd413657a https://qiita.com/jnchito/items/42193d066bd61c740612 https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5

Recommended Posts

[1st] RSpec beginners tried to write ModelSpec as a beginner
[3rd] RSpec beginners wrote SystemSpec as a beginner (final edition)
[2nd] RSpec beginners wrote SystemSpec as a beginner (swamp edition)
Rails beginners tried to get started with RSpec
To write a user-oriented program (1)
I tried to write code like a type declaration in Ruby
[RSpec on Rails] How to write test code for beginners by beginners
How to write a ternary operator
[RSpec] How to write test code
Java beginner tried to make a simple web application using Spring Boot
I want to write a nice build.gradle
[Basic] How to write a Dockerfile Self-learning ②
[Introduction to Java] How to write a Java program
I want to write a unit test!
[Java] Beginners want to make dating. 1st
How to write an RSpec controller test
[SpringBoot] How to write a controller test
Swift beginners tried to implement microwave logic!
Think of RxJava as a library that makes asynchronous processing easier to write
[Azure] I tried to create a Java application for free-Web App creation- [Beginner]
When defining a class, write formatTo as well as toString (how to use Formattable)