[RUBY] RSpec ~ Task model validation test creation

What is RSpec

It's a testing framework in Ruby.

In Ruby, there is another framework called Minitest by default. The Ruby on Rails Tutorial also teaches you how to test with Minitest. However, it seems that RSpec is overwhelmingly the testing framework actually used in the field.

This article describes how to install RSpec and perform validation testing on a model that has already been defined.

Technology version used

・ Ruby: 2.6.4 ・ Ruby on Rails: 5.2.3

Model overview

The models handled this time are the user model and the task model.

app/models/user.rb


class User < ApplicationRecord

  has_many :tasks, dependent: :destroy

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
  validates :email, uniqueness: true, presence: true

end

app/models/task.rb


class Task < ApplicationRecord
  belongs_to :user
  validates :title, presence: true, uniqueness: true
  validates :status, presence: true
  enum status: { todo: 0, doing: 1, done: 2 }
end

Since we are testing the validation of the task model this time, we will omit the explanation of the validation of the user model. As you can see from the task model, the title and status columns are validated (restricted).

title column presence: true => Value must be stored uniqueness: true => The stored value must be unique

status column presence: true => Value must be stored

Installation procedure

It's a bit long from here, but I'll show you the steps step by step.

Install RSpec and FactoryBot

RSpec is a testing framework as mentioned above. What is FactoryBot? When testing with RSpec, you can create a record of the model and use it in the test.

The installation procedure refers to the official README procedure. RSpec Official README Factory Bot Official README

Add rspec-rails and factory_bot_rails to both the : development and: test groups of the Gemfile.

Gemfile


group :development, :test do
  gem 'factory_bot_rails'
  gem 'rspec-rails', '~> 4.0.2'
  gem 'byebug', platforms: %i[mri mingw x64_mingw]
end

As usual bundle install

$ bundle install

In addition, run $ rails generate rspec: install

`rails generate rspec:install`
Running via Spring preloader in process 31987
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

This completes the installation.

Create a Task model spec file

$ rails generate rspec:model task
Running via Spring preloader in process 32051
      create  spec/models/task_spec.rb
      invoke  factory_bot
      create    spec/factories/tasks.rb

Just in case, let's check if the test of task_spec.rb can be executed.

$ bundle exec rspec spec/models/task_spec.rb
*

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) Task add some examples to (or delete) /Users/sakidendaiki/Downloads/RUNTEQ/tasks/option_tasks/sample_app_for_rspec/spec/models/task_spec.rb
     # Not yet implemented
     # ./spec/models/task_spec.rb:4


Finished in 0.0053 seconds (files took 1.6 seconds to load)
1 example, 0 failures, 1 pending

Create FactoryBot test data for task and user models

Create a file for the FactoryBot of the task model. If you want to create a FactoryBot file for your model, write ** $ rails g factory_bot: model model name **.

This time we are testing the task model, so let's execute the command $ rails g factory_bot: model task.

$ bin/rails g factory_bot:model task
Running via Spring preloader in process 28632
   identical  spec/factories/tasks.rb

Next, write the FactoryBot of task as follows.

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

By defining the above, you can call the task model defined above withFactoryBot.create (: task). Try it on the console.

$ FactoryBot.create(:task)
   (0.1ms)  SAVEPOINT active_record_1
  User Exists (0.7ms)  SELECT  1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]

Abbreviation

=> #<Task:0x00007f92a4342b08
 id: 1,
 title: "title_1",
 content: "content",
 status: "todo",
 deadline: Mon, 25 Jan 2021 07:24:22 UTC +00:00,
 created_at: Mon, 18 Jan 2021 07:24:22 UTC +00:00,
 updated_at: Mon, 18 Jan 2021 07:24:22 UTC +00:00,
 user_id: 1>

sequence (: title," title_1 ") stores the string" title_1 "in the title column once FactoryBot is called. When called a second time, the string will be "title_2". That is, it adds 1 to the last digit each time it is called.

The reason for doing this is that the title column has a validation of uniqueness: true. To prevent the value from being overlaid on the title column, the value in the title column is changed each time it is called. The sequence method is used when there is uniqueness: true like this.

The 1.week.from_now defined in the deadline column has a literal meaning. The time of the same time one week later is output. Rails is really easy to understand, isn't it?

association: user is very similar to a foreign key reference. The task model has a 1: many relationship with the user model, so it has a user_id column. By setting association: user, the test data of factory: user defined in the file of FactoryBot is referenced.

Given that Task also requires user_id, we need to create a FactoryBot for user as well.

bin/rails g factory_bot:model user
Running via Spring preloader in process 29285
      create  spec/factories/users.rb

Describe the FactoryBot of the user model

spec/factories/users.rb


FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user_#{n}@example.com" }
    password { "password" }
    password_confirmation { "password" }
  end
end

The description method of the sequence method is different from that described in the task model, but the content is the same. In other words, each time the factory of FactoryBot,: user, is called, the value of n increases steadily, so that the value of the email column becomes unique.

Now the test data is ready!

Added --format documentation (1 before test description)

Before writing a test for RSpec, add--format documentationto the.rspecfile.

.rspec


--require spec_helper
--format documentation

This will change the test execution result to document format. Simply put, ** test results are easier for humans to read. ** **

Added config.include FactoryBot :: Syntax :: Methods (before test description part 2)

When writing a test, the test data must be called as FactoryBot, such asFactoryBot.create (: task)orFactoryBot.build (: task). However, it is verbose to write each time you call the test data. So put config.include FactoryBot :: Syntax :: Methods in ruby: spec/rails_helper.rb.

spec/rails_helper.rb


config.include FactoryBot::Syntax::Methods

By doing this, when calling the test data, you can omit the description of FactoryBot, such ascreate (: task).

Create a test for model validation in task_spec.rb

Finally write the test.

spec/models/task_spec.rb


require 'rails_helper'

RSpec.describe Task, type: :model do
  describe 'validation' do
    it 'Valid if all attributes are stored properly' do
      task = build(:task)
      expect(task).to be_valid
      expect(task.errors).to be_empty
    end

    it '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

    it 'Invalid if there is no status' do
      task_without_status = build(:task, status: nil)
      expect(task_without_status).to be_invalid
      expect(task_without_status.errors[:status]).to eq ["can't be blank"]
    end

    it 'Invalid if the same title is duplicated' do
      task = create(:task)
      task_with_duplicated_title = build(:task, title: task.title)
      expect(task_with_duplicated_title).to be_invalid
      expect(task_with_duplicated_title.errors[:title]).to eq ["has already been taken"]
    end

    it 'Valid if the title is another name' do
      task = create(:task)
      task_with_another_title = build(:task, title: 'another_title')
      expect(task_with_another_title).to be_valid
      expect(task_with_another_title.errors).to be_empty
    end
  end
end

The content of the test is described in the section ** it'~~~~~~' do **. The first test'valid if all values ​​are stored properly'is calling and testing the test data as is. The test data should be valid because the values ​​are defined with validation in mind.

I think you can intuitively understand expect (task) .to be_valid and expect (task.errors) .to be_empty. We are testing that the variable task is be_valid and task.errors is be_empty (empty). If you're stuck with validation, task.errors should contain some error message.

What about expect (task_without_title) .to be_invalid andexpect (task_without_title.errors [: title]). to eq ["can't be blank"]? I think it's easy to guess.

Check test results

Test with RSpec. Just run the $ bundle exec rspec into your terminal.

$ bundle exec rspec 

Task
  validation
    is valid with all attributes
    is invalid without title
    is invalid without status
    is invalid with a duplicate title
    is valid with another title

Finished in 0.14338 seconds (files took 1.39 seconds to load)
5 examples, 0 failures

Remove validation to see if test fails

There is a way to make the test fail to make sure it is really correct. For example, the second test'invalid without title'successes because there is a validation of presence: true on the title column of the task model. Now let's remove presence: true.

  validates :title, uniqueness: true

[Test execution result]

Failures:

  1) Task validation is invalid without title
     Failure/Error: expect(task.errors[:title]).to include("can't be blank")
       expected [] to include "can't be blank"
     # ./spec/models/task_spec.rb:12:in `block (3 levels) in <top (required)>'

Finished in 0.10893 seconds (files took 0.7813 seconds to load)
5 examples, 1 failure

Failed examples:

rspec ./spec/models/task_spec.rb:9 # Task validation is invalid without title

By failing a successful test, you can prove that the test you wrote was the correct one.

Recommended Posts

RSpec ~ Task model validation test creation
Memorandum (task: model test code)
[Ruby on Rails] Model test with RSpec
[Rails5] Rspec -validation-
RSpec test code execution
Model and table creation
Introduction to RSpec 1. Test, RSpec
Testing model with RSpec
[Rails / RSpec] Write Model test (using Shoulda Matchers / FactoryBot)
[Rails] Test with RSpec
Test Nokogiri with Rspec.
About method matchers used in model unit test code (RSpec)
Introduction to RSpec 3. Model specs
Test Active Strage with RSpec
[Rails] Test code using Rspec
Test GraphQL resolver with rspec
[Rails] About Rspec response test
Spring single item validation test
Task management application creation procedure