Home Fitness Training Fast Specs with Automated Testing Tools

Fast Specs with Automated Testing Tools

29 min read
0
0
3


by Martin Führlinger, Backend Engineer

In the backend staff we normally attempt to automate issues. Therefore we’ve got tons of assessments to confirm correctness of our code in our gems and providers. Automated assessments are executed a lot quicker and with a lot larger protection than any tester can do manually in an analogous time. Over the years numerous performance has been added, and consequently, numerous assessments have been added. This led to our take a look at suites changing into slower over time. For instance, we’ve got a service the place round 5000 assessments take about 8 minutes. Another service takes about 15 minutes for round 3000 assessments. So why is service A so quick and repair B so sluggish? 

In this weblog put up I’ll present some dangerous examples of methods to write a take a look at, and methods to enhance automated testing instruments to make assessments quicker. The Runtastic backend staff normally makes use of `rspec` together with `factory_bot` and jruby

The Test File Example

The following code reveals a small a part of an actual instance of a take a look at file we had in our take a look at suite. It creates some customers and tries to search out them with the UserSearch use case.

describe  Users::UseCase::UserSearch::ByAnyEmail do
 describe "#run!" do
   topic { Users::UseCase::UserSearch::ByAnyEmail.new(search_criteria).run! }
   let(:current_user_id) { nil }
   let(:search_criteria) { double(question: question, dimension: dimension, quantity: quantity, current_user_id: current_user_id) }
   let(:default_photo_url) { "#{Rails.configuration.services.runtastic_web.public_route}/assets/user/default_avatar_male.jpg" }
   def expected_search_result_for(person)
     UserSearchResultWrapper.new(person.attributes.merge("avatar_url" => default_photo_url))
   finish
   shared_examples "find users by email" do
     it { anticipate(topic.class).to eq UserSearchResult }
     it { anticipate(topic.customers).to return_searched_users expected_users }
     it { anticipate(topic.more_data_available).to eq more_data? }
   finish
   let!(:s_m_user)           { FactoryBot.create :person, e-mail: "s.m@mail.com" }
   let(:runner_gmail_user)  { FactoryBot.create :person, google_email: "runner@gmail.at" }
   let!(:su_12_user)         { FactoryBot.create :person, e-mail: "su+12@gmx.at" }
   let(:su_12_google_user)   { FactoryBot.create :person, google_email: "su+12@gmx.at" }
   let!(:user_same_mail) do
     FactoryBot.create :person, e-mail: "user@rt.com", google_email: "person@rt.com”
   finish
   let!(:combined_user) do
     FactoryBot.create :person, e-mail: "user1@rt.at", google_email: "user1@google.at"
   finish
   let!(:johnny_gmail_user)  { FactoryBot.create :person, google_email: "johnny@gmail.com" }
   let!(:jane_user)          { FactoryBot.create :person, e-mail: "jane@email.at" }
   let!(:zorro)              { FactoryBot.create :person, e-mail: "zorro@example.com" }
   earlier than do
     FactoryBot.create(:person, google_email: "jane@email.at").faucet do |u|
       u.update_attribute(:deleted_at, 1.day.in the past)
     finish
     runner_gmail_user
     su_12_google_user
   finish
   context "the query is '123'" do
     it_behaves_like "find users by email" do
       let(:dimension)     { 4 }
       let(:quantity)   { 1 }
       let(:question) { [123] }
       let(:expected_users) { [] }
       let(:more_data?) { false }
     finish
   finish
   context "the query contains invalid emails" do
     it_behaves_like "find users by email" do
       let(:question) do
         ["s.m@mail.com", "su+12gmx.at", "", "'", "johnny@gmail.com"]
       finish
       let(:dimension)   { 50 }
       let(:quantity) { 1 }
       let(:expected_users) do
         [
           expected_search_result_for(s_m_user),
           expected_search_result_for(johnny_gmail_user)
         ]
       finish
       let(:more_data?) { false }
     finish
   finish
 finish
finish

So let’s analyze the take a look at: It has a topic, which signifies what to check. In this case: Run the use case and return the outcome. It defines a shared example which accommodates the precise assessments. These shared examples assist as a result of the assessments are grouped collectively and they are often reused. This method it’s doable to simply arrange the assessments with totally different parameters and name the instance by utilizing it_behaves_like. The take a look at above accommodates some person objects created with let and a earlier than block, which known as earlier than every take a look at. The it-block accommodates two contexts to explain the setup and calls the shared instance as soon as per context. So mainly this take a look at runs 6 assessments (3 assessments within the shared_example, known as twice). Running them regionally on my laptop computer outcomes on this:

Users::UseCase::UserSearch::ByAnyEmail
 #run!
   the question is '123'
     behaves like discover customers by e-mail
       ought to return searched customers
       ought to eq false
       ought to eq UserSearchResult
   the question accommodates invalid emails
     behaves like discover customers by e-mail
       ought to eq UserSearchResult
       ought to eq false
       ought to return searched customers #<UserSearchResultWrapper:0x7e9d3832 @avatar_url="http://localhost.runtastic.com:3002/assets/user/def....jpg", @country_id=nil, @gender="M", @id=51, @last_name="Doe-51", @first_name="John", @guid="ab
c51"> and #<UserSearchResultWrapper:0x33c8a528 @avatar_url="http://localhost.runtastic.com:3002/assets/user/def....jpg", @country_id=nil, @gender="M", @id=55, @last_name="Doe-55", @first_name="John", @guid="abc55">
Finished in 34.78 seconds (recordsdata took 20.66 seconds to load)
6 examples, 0 failures

So about 35 seconds for 6 assessments.

Let vs Let! 

As you may see, we’re utilizing let! and let. The distinction between these two strategies is, that let! at all times executes, and let solely executes if the reference is used. In the above instance:

let!(:s_m_user)
let(:runner_gmail_user)

“s_m_user” is created at all times, “runner_gmail_user” is created provided that used. So the above let! usages are creating 7 customers for the assessments.

Before Block

The before block can also be executed each time earlier than the take a look at. If nothing is handed to the earlier than technique, it defaults to :every. The above earlier than block creates a person, and references 2 different customers, which then instantly are created, too. 

So we’re creating 10 customers for every take a look at. 

rspec-it-chains

As each it is a single take a look at, the shared instance accommodates 3 single assessments. Every take a look at will get a clear state, so the customers are created once more for every take a look at. Having a number of it blocks one after one other, referring to the identical topic, someway appears to be like like a series.

Test setup

What do the assessments truly do? The first one passes a web page dimension of 4 with a question “123” to the search use case and expects, as no person has 123 within the e-mail attribute, no customers to be discovered.

context "the query is '123'" do
     it_behaves_like "find users by email" do
       let(:dimension)     { 4 }
       let(:quantity)   { 1 }
       let(:question) { [123] }
       let(:expected_users) { [] }
       let(:more_data?) { false }
     finish
   End

So we’re creating 3 instances (3 it blocks) 10 customers however anticipate no person to be discovered.

The second context passes among the emails, and a few invalid ones into the search, and anticipate 2 customers to be discovered. 

context "the query contains invalid emails" do
    it_behaves_like "find users by email" do
      let(:question) do
        ["s.m@mail.com", "su+12gmx.at", "", "'", "johnny@gmail.com"]
      finish
      let(:dimension)   { 50 }
      let(:quantity) { 1 }
      let(:expected_users) do
        [
          wrap(s_m_user),
          wrap(johnny_gmail_user)
        ]
      finish
      let(:more_data?) { false }
    finish
  finish

So we’re creating 3 instances 10 customers to have the ability to discover 2 of them in a single take a look at and get the appropriate flag in one other take a look at.

Having a better have a look at the shared_example:

it { anticipate(topic.class).to eq UserSearchResult }
 it { anticipate(topic.customers).to return_searched_users expected_users }
 it { anticipate(topic.more_data_available).to eq more_data? }

you may see that the primary one is just not even anticipating something user-related to be returned. It simply expects the use-case to return a selected class. The second one truly assessments if the outcome accommodates the customers we wish to discover. The third it block checks if the more_data_available flag is ready correctly.

Overall, we’ve got 6 assessments, needing 35 seconds to run, creating 10 customers for every take a look at (60 customers solely) and calling the topic 6 instances, and we mainly solely look forward to finding 2 customers as soon as.

Obviously, this may be improved.

Improvement

First of all, let’s eliminate the it chain, mix it inside one it block.

shared_examples "find users by email" do
  it "returns user info" do
    anticipate(topic.class).to eq UserSearchResult
    anticipate(topic.customers).to return_searched_users expected_users
    anticipate(topic.more_data_available).to eq more_data?
  finish
finish

Combining it blocks is sensible in the event that they normally take a look at an analogous factor (as above). For instance, doing a request and anticipating some response body and standing 200 doesn’t have to be two separate assessments. Combining two it blocks which take a look at one thing totally different, nevertheless, doesn’t make sense, reminiscent of assessments for the response code of a request and if that request saved the information appropriately within the database.

This leads to the assessments ending inside ~ 15 seconds, solely 2 examples.

The subsequent step is to not create the customers if they aren’t wanted. Therefore let’s swap to let as an alternative of let!. Also take away the earlier than block as it’s, and solely create some correct quantity of customers essential for the take a look at. The assessments appear to be this in finish:

describe  Users::UseCase::UserSearch::ByAnyEmail do
 describe "#run!" do
   topic { Users::UseCase::UserSearch::ByAnyEmail.new(search_criteria).run! }
   let(:current_user_id) { nil }
   let(:search_criteria) { double(question: question, dimension: dimension, quantity: quantity, current_user_id: current_user_id) }
   let(:default_photo_url) { "#{Rails.configuration.services.runtastic_web.public_route}/assets/user/default_avatar_male.jpg" }
   def expected_search_result_for(person)
     UserSearchResultWrapper.new(person.attributes.merge("avatar_url" => default_photo_url))
   finish
   shared_examples "find users by email" do
     it "return user info" do
       anticipate(topic.class).to eq UserSearchResult
       anticipate(topic.customers).to return_searched_users expected_users
       anticipate(topic.more_data_available).to eq more_data?
     finish
   finish
   let(:s_m_user)           { FactoryBot.create :person, e-mail: "s.m@mail.com" }
   let(:runner_gmail_user) { FactoryBot.create :person, google_email: "runner@gmail.at" }
   let(:su_12_user)         { FactoryBot.create :person, e-mail: "su+12@gmx.at" }
   let(:su_12_google_user)  { FactoryBot.create :person, google_email: "su+12@gmx.at" }
   let(:user_same_mail) do
     FactoryBot.create :person, e-mail: "user@rt.com", google_email: "user@rt.com"
   finish
   let(:combined_user) do
     FactoryBot.create :person, e-mail: "user1@rt.at", google_email: "user1@google.at"
   finish
   let(:johnny_gmail_user)  { FactoryBot.create :person, google_email: "johnny@gmail.com" }
   let(:jane_user)          { FactoryBot.create :person, e-mail: "jane@email.at", fb_proxied_email: "jane@fb.at" }
   let(:zorro)              { FactoryBot.create :person, e-mail: "zorro@example.com" }
   let(:deleted_user) do
     FactoryBot.create(:person, google_email: "jane@email.at").faucet do |u|
       u.update_attribute(:deleted_at, 1.day.in the past)
     finish
   finish
   context "the query is '123'" do
     earlier than do
       s_m_user
     finish
     it_behaves_like "find users by email" do
       let(:dimension)     { 4 }
       let(:quantity)   { 1 }
       let(:question) { [123] }
       let(:expected_users) { [] }
       let(:more_data?) { false }
     finish
   finish
   context "the query contains invalid emails" do
     earlier than do
       s_m_user
       su_12_user
       johnny_gmail_user
     finish
     it_behaves_like "find users by email" do
       let(:question) do
         ["s.m@mail.com", "su+12gmx.at", "", "'", "johnny@gmail.com"]
       finish
       let(:dimension)   { 50 }
       let(:quantity) { 1 }
       let(:expected_users) do
         [
           expected_search_result_for(s_m_user),
           expected_search_result_for(johnny_gmail_user)
         ]
       finish
       let(:more_data?) { false }
     finish
   finish
 finish
finish

And lead to 

Users::UseCase::UserSearch::ByAnyEmail
 #run!
   the question is '123'
     behaves like discover customers by e-mail
       return person data
   the question accommodates invalid emails
     behaves like discover customers by e-mail
       return person data                                                                                                                                                                                                                                        
Finished in 8.16 seconds (recordsdata took 22.34 seconds to load)
2 examples, 0 failures

As you may see, I do create customers, even when I don’t anticipate them to be within the outcome, to show the correctness of the use case. But I don’t create 10 per take a look at, solely 1 and 3. Some of the above customers aren’t created (or used) in any respect now, however as the unique take a look at file accommodates extra assessments, which ultimately want them once more for different contexts, I stored them within the instance too.

So now we solely create 4 customers, as an alternative of 60. By simply adapting the code a bit, we’ve got the identical take a look at protection with solely 2 assessments as an alternative of 6, and solely needing 8 as an alternative of 35 seconds, which is 77% much less time.

FactoryBot: create vs construct vs attribute_for

As you may see above, we’re utilizing FactoryBot closely to create objects through the assessments.

let(:person) { FactoryBot.create(:person) }

This creates a brand new person object as quickly as `person` is referenced within the assessments. The downside of this line is that it actually creates the person within the database, which is fairly typically not essential. The higher strategy, if relevant, can be to solely construct the item with out storing it:

let(:person) { FactoryBot.construct(:person) }

Obviously this doesn’t work in the event you want the item within the database, as for the take a look at instance above, however that extremely is determined by the take a look at. Another much less recognized function of FactoryBot is to create solely the attributes for an object, represented as hash.

let(:user_attrs) { FactoryBot.attributes_for(:person) }

This would create a hash containing the attributes for a person. It doesn’t even create a User object, which is even quicker than construct. 

A doable easy take a look at can be:

describe User do
  50.instances do  
    topic { FactoryBot.create(:person) }
    it { anticipate(topic.has_first_login_dialog_completed).to eq(false) }
  finish
finish

As the has_first_login_dialog_completed technique solely wants some attributes set on a person, irrespective of whether it is saved in a database, a construct can be a lot quicker than a create, working the take a look at 100 instances to additionally use the impact of the just-in-time compiler of the used jruby interpreter. This method the actual distinction between create and construct is extra seen. So switching from .create to .construct saves about 45% of the execution time.

Finished in 1 minute 1.61 seconds (recordsdata took 23.4 seconds to load)
100 examples, 0 failures
Finished in 34.87 seconds (recordsdata took 21.69 seconds to load)
100 examples, 0 failures

Summary

So easy enhancements within the assessments can result in a pleasant efficiency enhance working them.

  • Avoid it-chains if the assessments correlate to one another
  • Avoid let! in favor of let, and create the objects inside earlier than blocks when essential
  • Avoid earlier than blocks creating numerous stuff which will not be essential for all assessments
  • Use FactoryBot.construct as an alternative of .create if relevant.

Keep an eye fixed in your test-suite and don’t hesitate to take away duplicate assessments, perhaps already out of date assessments. As (in our case) the assessments are working earlier than each merge and on each commit, attempt to maintain your take a look at suite quick. 

***



Source link

Load More Related Articles
Load More By Neil Johnson
Load More In Fitness Training

Leave a Reply

Your email address will not be published. Required fields are marked *

Check Also

30 Minute Flow to Soothe Joints

Show your hard-working joints some love with this mild move from yoga instructor Nicole Ca…