It is a memorandum.
References Everyday Rails-Introduction to Rails Testing with RSpec https://leanpub.com/everydayrailsrspec-jp
It is a method to eliminate code duplication, and standardizes processing (login processing, etc.). For example, if there are five tests that perform the login process in advance, when the login button is changed, five changes are required. Separating this into a single module improves readability while keeping modifications to a minimum.
Try separating the following login process into modules
#Get login screen
visit root_path
#Click sign in
click_link "Sign in"
#Enter the user's email address in the Email text field
fill_in "Email", with: user.email
#Enter password as above
fill_in "Password", with: user.password
#Click the login button
click_button "Log in"
Suppose the code in the methods described in this module is scattered all over the test.
To separate this code, create spec / supprot / login_support.rb and modularize it.
login_support.rb
module LoginSupport
def sign_in_as(user)
visit root_path
click_link "Sign in"
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Log in"
end
#Read entire RSpec
RSpec.configure do |config|
config.include LoginSupport
end
↓
#If you want to read it explicitly for each test, in the spec file you want to read
include LoginSupport
#If so, it will be read.
Now you can call the methods (helper methods) in the module. Let's compare before and after rewriting. Everyday Rails-Borrow the code to get started with Rails testing with RSpec.
Before replacement
projects_spec.rb
require 'rails_helper'
RSpec.feature "Projects", type: :feature do
scenario "user creates a new project" do
user = FactoryBot.create(:user)
#from here
visit root_path
click_link "Sign in"
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Log in"
#Replace up to here with helper method
expect {
click_link "New Project"
fill_in "Name", with: "Test Project"
fill_in "Description", with: "Trying out Capybara"
click_button "Create Project"
expect(page).to have_content "Project was successfully created"
expect(page).to have_content "Test Project"
expect(page).to have_content "Owner: #{user.name}"
}.to change(user.projects, :count).by(1)
end
end
After replacement
projects_spec.rb
require 'rails_helper'
RSpec.feature "Projects", type: :feature do
scenario "user creates a new project" do
user = FactoryBot.create(:user)
#from here
sign_in_as(user)
#Or
sign_in_as user
#But OK
#I replaced the above with a helper method
expect {
click_link "New Project"
fill_in "Name", with: "Test Project"
fill_in "Description", with: "Trying out Capybara"
click_button "Create Project"
expect(page).to have_content "Project was successfully created"
expect(page).to have_content "Test Project"
expect(page).to have_content "Owner: #{user.name}"
}.to change(user.projects, :count).by(1)
end
end
The important thing here is to make the method name easy to understand. It is important to keep in mind a name that you can easily understand, because you cannot understand the operation by the name of the method, and going to refer to the original file will only make the test inconvenient.
In some cases, the data required for the test is set up before the test (data creation process such as user creation) before executing describe or context. Since before is executed every time describe or context is executed, the following problems may occur. -Because it is executed every time, it may have an unexpected effect on the test. -There is a high risk that the performance of the test will be reduced because it is executed every time. -The amount of code written increases in proportion to the amount of test, which impairs cost and readability.
Lazy loading by let clears this. Since let is executed only when it is called, the above problem can be solved. Let's compare before and after rewriting. Everyday Rails-Borrow the code to get started with Rails testing with RSpec.
let unused
note_spec.rb
require 'rails_helper'
RSpec.describe Note, type: :model do
before do
#User created
@user = User.create(
first_name: "Joe",
last_name: "Tester",
email: "[email protected]",
password: "dottle-nouveau-pavilion-tights-furze",
)
#Project creation
@project = @user.projects.create(
name: "Test Project",
)
end
it "Must be valid if there are users, projects, messages" do
note = Note.new(
message: "This is a sample note.",
user: @user,
project: @project,
)
expect(note).to be_valid
end
it "If there is no message, it is in an invalid state" do
note = Note.new(message: nil)
note.valid?
expect(note.errors[:message]).to include("can't be blank")
end
describe "Search for messages that match the string" do
before do
@note1 = @project.notes.create(
message: "This is the first note.",
user: @user,
)
@note2 = @project.notes.create(
message: "This is the second note.",
user: @user,
)
@note3 = @project.notes.create(
message: "First, preheat the oven.",
user: @user,
)
end
context "When finding matching data" do
it "Returning notes that match the search string" do
expect(Note.search("first")).to include(@note1, @note3)
end
end
context "When no matching data is found" do
it "Returning an empty collection" do
expect(Note.search("message")).to be_empty
end
end
end
end
use let
note_spec.rb
require 'rails_helper'
RSpec.describe Note, type: :model do
#use let instead of before
let(:user) { FactoryBot.create(:user) }
let(:project) { FactoryBot.create(:project, owner: user) }
it "Must be valid if there are users, projects, messages" do
note = Note.new(
message: "This is a sample note.",
user: user,
project: project,
)
expect(note).to be_valid
end
it "If there is no message, it is in an invalid state" do
note = Note.new(message: nil)
note.valid?
expect(note.errors[:message]).to include("can't be blank")
end
describe "Search for messages that match the string" do
#use let instead of before
let(:note1) {
FactoryBot.create(:note,
project: project,
user: user,
message: "This is the first note."
)
}
let(:note2) {
FactoryBot.create(:note,
project: project,
user: user,
message: "This is the second note."
)
}
let(:note3) {
FactoryBot.create(:note,
project: project,
user: user,
message: "First, preheat the oven."
)
}
context "When finding matching data" do
it "Returning notes that match the search string" do
expect(Note.search("first")).to include(note1, note3)
end
end
context "When no matching data is found" do
it "Returning an empty collection" do
expect(Note.search("message")).to be_empty
end
end
end
end
When using before, instance variables were used to cross the scope in the test, but in let, it is described by variables. Now that the test passes, one problem arises here. Add expect (Note.count) .to eq 3 to the last "returning an empty collection".
note_spec.rb
context "When no matching data is found" do
it "Returning an empty collection" do
expect(Note.search("message")).to be_empty
#Add this sentence
expect(Note.count).to eq 3
end
end
end
end
The above description does not pass the test. The reason is simple: let is lazy loading.
In short, no data is created unless called.
With expect (Note.count) .to eq 3, the test goes to the DB to find the data. Naturally, the DB is empty because let is not called. However, since the definition of expectation is that the number of data in note is equal to 3, the number of data is 0, the test does not pass, and an error occurs.
One way to solve this is to simply call let, An example using let! is shown below.
note_spec.rb
#let instead of let!use
let!(:note1) {
FactoryBot.create(:note,
project: project,
user: user,
message: "This is the first note."
)
}
let!(:note2) {
FactoryBot.create(:note,
project: project,
user: user,
message: "This is the second note."
)
}
let!(:note3) {
FactoryBot.create(:note,
project: project,
user: user,
message: "First, preheat the oven."
)
}
#====abridgement=====
context "When no matching data is found" do
it "Returning an empty collection" do
expect(Note.search("message")).to be_empty
#Add this sentence
expect(Note.count).to eq 3
end
end
end
end
This will pass the test. Let! preloads while let is lazy loading. The image is the same as before, and data is created for each evaluation block.
This means that the problems mentioned above have not been cleared. In this case, whether to use before or let! Depends on the situation. From the point of view of myself learning, I feel that it is okay to use before and let properly.
As a finding, basically let! I want to proceed in the direction of not using. I'd like to leave it to before when pre-loading is necessary, rather than making misreading or pocha mistakes because there is only a subtle difference between let and let !.
You can use shared_context to do the required setup with multiple test files. The usage is similar to the support module. Let's compare before and after rewriting. Everyday Rails-Borrow the code to get started with Rails testing with RSpec.
tasks_controller.rb
require 'rails_helper'
RSpec.describe TasksController, type: :controller do
#Summarize the setup here
let(:user) { FactoryBot.create(:user) }
let(:project) { FactoryBot.create(:project, owner: user) }
let(:task) { project.tasks.create!(name: "Task task") }
describe "#show" do
it "responds with JSON formatted output" do
sign_in user
get :show, format: :json,
params: { project_id: project.id, id: task.id }
expect(response.content_type).to eq "application/json"
end
end
describe "#create" do
it "responds with JSON formatted output" do
new_task = { name: "New test task" }
sign_in user
post :create, format: :json,
params: { project_id: project.id, task: new_task }
expect(response.content_type).to eq "application/json"
end
it "adds a new task to the project" do
new_task = { name: "New test task" }
sign_in user
expect {
post :create, format: :json,
params: { project_id: project.id, task: new_task }
}.to change(project.tasks, :count).by(1)
end
it "requires authentication" do
new_task = { name: "New test task" }
# Don't sign in this time ...
expect {
post :create, format: :json,
params: { project_id: project.id, task: new_task }
}.to_not change(project.tasks, :count)
expect(response).to_not be_success
end
end
end
Create a separate file and combine the 3 lines of let with shared_context Create spec / support / contexts / project_setup.rb
project_setup.rb
RSpec.shared_context "project setup" do
let(:user) { FactoryBot.create(:user) }
let(:project) { FactoryBot.create(:project, owner: user) }
let(:task) { project.tasks.create!(name: "Task task") }
end
Go back to tasks_controller.rb and rewrite the let part.
tasks_controller.rb
require 'rails_helper'
RSpec.describe TasksController, type: :controller do
#Delete let and project_setup.Include rb.
include_context "project setup"
#====Omitted below=====
Now you have multiple test files to combine the required setups into one.
・ Support module Common processing such as helper methods can be cut out in a separate file and included if necessary. ・ Let You can create the data used for the test. Impact on testing due to lazy loading Can be expected to improve performance. ・ Shared_context It is possible to put together the setup used for testing. Cut it out to another file and use it as an include.
Thank you for staying with us until the end. If you have any suggestions or advice, I would appreciate it if you could comment.
Recommended Posts