Unit testing is an integral part of every application. A system with unit tests that runs quickly and efficiently will save a lot of time deploying for the team during development. Here I suggest some ways to be able to optimize unit tests in rails:
Use: create,: build and: build_stubbed logically
create:
1 2 | FactoryBot.create(:comment) |
In this case a comment object will be created. It and all associations will be saved in Database.
build:
1 2 | FactoryBot.build(:comment) |
In this case, the comment object will not be saved to the Database but its associations do.
1 2 3 4 5 6 7 8 9 | factory :comment do association :post end FactoryBot.build(:comment) (0.1ms) begin transaction Post Create (0.5ms) INSERT INTO "posts" DEFAULT VALUES (0.6ms) commit transaction |
build_stubbed:
1 2 | FactoryBot.build_stubbed(:comment) |
: build_stubbed will not call the Database. It will create and assign properties to an object to make the object behave the same as the object created by : create . Its associations will also be created with : build_stubbed so there is absolutely no impact on the database.
1 2 3 4 5 | comment = FactoryBot.build_stubbed(:comment) #<Comment:0x007f94d2b92df0 id: 1002, post_id: 1001, body: "text"> comment.post #<Post:0x007f94d5883440 id: 1001, name: nil> |
When to use: build_stubbed?
We have the model:
1 2 3 4 5 6 7 8 9 10 11 12 | class Comment < ApplicationRecord belongs_to :post def full_comment content + post.name end end class Post < ApplicationRecord has_many :comments end |
Factory:
1 2 3 4 5 6 7 8 9 | factory :comment do content: {"A"} association :post end factory :post do name: {"B"} end |
Use rspec to test for the full_comment method:
1 2 3 4 5 6 | ... describe ".full_comment" do let!(:comment) {FactoryBot.build_stubbed(:comment)} it {expect(comment.full_comment).to eq("AB")} end |
In most cases it is not necessary to save the object to the Database. So when writing tests we should restrict the use of : create but instead use : build and : build_stubbed to reduce dependence on the Database. This makes the test run faster and more stable.
Eliminate unnecessary associations
In the above example we have Factory:
1 2 3 4 5 | factory :comment do content: {"A"} association :post end |
Every time we create a comment object, a post object will be created as well. This will affect the performance of the test case because it is not always necessary to use its association.
To avoid this, we can fix the factory to:
1 2 3 4 5 6 7 | factory :comment do content: {"A"} trait :with_post do association :post end end |
Now whenever you want to create a comment with a post just:
1 2 | FactoryBot.create(:comment, :with_post) |
Use before (: all) to remove let! unnecessary
If the test sample in the first example has more test cases:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ... describe ".full_comment" do let!(:comment) {FactoryBot.build_stubbed(:comment)} it {expect(comment.full_comment).to eq("AB")} context "2" do # case 2 end context "3" do # case 3 end end |
At this time let! will create a comment object for each case (a total of 3 objects) while maybe we just need to use one. We can edit the code as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ... describe ".full_comment" do before(:all) {@comment = FactoryBot.build_stubbed(:comment)} it {expect(@comment.full_comment).to eq("AB")} context "2" do # case 2 end context "3" do # case 3 end end |
Now all testcase will use the same comment object.
However, you should be cautious when using before (: all) , in this case, doing the “global” comment object between test cases, test cases can change the properties of this object, leading to mistakes. skew the results of other test cases.
Use Mock and stub
If you do not want to execute a time-consuming method (such as call API, getting results from another heavily-handled module) you can fake the results returned by stub:
1 2 3 4 5 6 7 8 9 | allow(book).to receive(:title) { "The RSpec book" } allow(book).to receive(:title).and_return("The RSpec book") allow(book).to receive_messages( title: "The RSpec book", description: "Write tests use method stubs" ) |
You can use receive_message_chain instead of receive to stub a sequence of methods as follows:
1 2 3 4 5 6 7 8 | allow(book).to receive_message_chain("title.length") { 30 } allow(book).to receive_message_chain(:title, :length => 30) allow(book).to receive_message_chain(:title, :length) { 30 } book.title.length # => 30 |
Refer:
https://medium.com/appaloosa-store-engineering/tips-to-improve-speed-of-your-test-suite-8418b485205c
https://relishapp.com/rspec/rspec-core/v/3-6/docs/helper-methods/let-and-let
https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md
https://medium.com/@DmytroVasin/speed-up-your-tests-via-build-stubbed-f1926863b3d7
https://www.netguru.com/blog/9-ways-to-speed-up-your-rspec-tests