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.
・ Ruby: 2.6.4 ・ Ruby on Rails: 5.2.3
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
enum
, it is called an enumeration type, and you can give an alias to a numerical column programmatically.
status
stores the integer
type, but what is the numerical value of status
for humans, such as" 0 is the status of todo, 1 is the status of doing, ... " It's hard to understand if it's represented. Therefore, it is treated as a numerical value on the DB, but from the programmer's point of view, it is treated as a character string. enum
is used. For details, refer to here.It's a bit long from here, but I'll show you the steps step by step.
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.
$ 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 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!
Before writing a test for RSpec
, add--format documentation
to the.rspec
file.
.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. ** **
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)
.
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.
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
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