Tại sao rspec test của bạn bị chậm

Bạn có bao giờ thấy ngại việc chạy rspec tốn quá nhiều thời gian, 30 phút hoặc hơn nữa? Có thể sẽ bình thường nếu như bạn chỉ chạy lại để kiểm tra và khá chắc chắn là nó sẽ pass. Nhưng nếu như nó fail ở một vài case hoặc mình cần sửa lại cho phù hợp với logic hiện tại. Chắc chắn bạn sẽ phải chạy đi chạy lại nhiều lần một số file spec mà trường hợp đen đủi còn phải chạy lại tất cả. Xin chúc mừng vì lúc đó bạn đã trúng được rổ hành kha khá lớn (facepalm). Nguyên nhân của việc rspec chạy chậm, ngoài việc code spec lởm thì cũng có một số yếu tố ảnh hưởng rất lớn đến tốc độ chạy rspec mà thường chúng ta có thể không để ý tới.

Chậm do configure trong spec_helper

Hãy xét ví dụ sau:

# spec/models/user_spec.rb

describe "associations" do
  subject(:user) { User.new }

  it { should have_many(:orders) }
  # 12 additional association specs
end

---> run rspec1 Mất hơn 3s để test những quan hệ của model, thậm chí còn không đụng vào database, quá lâu. Còn đây là file spec_helper rất lớn, và trong nó chứa rất nhiều thủ phạm dẫn đến việc rspec chạy chậm

# spec/spec_helper.rb`

RSpec.configure do |config|
  # > 300 lines of around, before and after blocks
end

Sau khi chỉnh sửa và chạy lại rspec thì rspec2 Tốc độ đã tăng lên gần 9 lần. Bây giờ chúng ta sẽ bắt đầu nói về các nguyên nhân cũng như cách giải quyết những vấn đề dẫn đến giảm tốc độ chạy rspec trong spec_helper

Culprit thứ nhất

# spec/spec_helper.rb

config.before(:each) do
  stub_something
end

Khối lệnh before ở trên chỉ cần dùng cho feature spec nhưng nếu để như trên thì nó sẽ chạy trước tất cả các single spec. Chúng ta cần cô lập để nó chỉ chạy cho spec có type là feature như sau

# spec/spec_helper.rb

config.before(:each, type: :feature) do
  stub_something
end

Tuy nhiên không phải feature spec nào cũng cần chạy khối lệnh trên, vì vậy chúng ta có thể cô lập nó tốt hơn bằng cách định nghĩa ra một metadata riêng

# spec/spec_helper.rb

config.before(:each, :stub_something) do
  stub_something
end

Ở chỗ nào cần chạy khối lệnh trên thì dùng như sau

# spec/features/user_does_something_spec.rb

scenario "user does something", :stub_something do
  # spec logic
end

Culprit thứ hai

# spec/spec_helper.rb

config.before(:each) do |example|
  DatabaseCleaner.strategy = :truncation
end

Sau mỗi spec, có thể sử dụng các strategy(chiến lược) khác nhau của DatabaseCleaner để "đặt lại" cơ sở dữ liệu. Chúng tôi sử dụng một chiến lược rất chậm là :truncation. Để sử dụng chiến lược :truncation cho những chỗ cần, hãy sử dụng cách định nghĩa ra một metadata riêng như ở culprit thứ nhất

# spec/spec_helper.rb

config.before(:each, :clean_database_with_truncation) do |example|
  DatabaseCleaner.strategy = :truncation
end

Chậm trong Spec Setup

Sử dụng flag profile của RSpec, chúng ta có thể xác định các thông số chậm nhất khi chạy rspec cho file.

rspec spec/models/user_spec.rb --profile 5

rspec3 Ở trong các spec chậm nhất có

# spec/models/user_spec.rb

it "does something" do
  user = FactoryGirl.create(:user)

  # exercise on user

  # expectation on user
end

Chúng tôi đã comment thử tất cả ngoại trừ dòng sử dụng FactoryGirl và tốc độ không cải thiện chút nào, vậy nguyên nhân chậm rất có thể là do FactoryGirl. Trong quá trình xử lý vấn đề này, chúng tôi đã xem xét liệu có cần dùng FactoryGirl hay có thể dùng build_stubbed. Ngoài ra chúng tôi cũng vẫn xem xét xem có thể tối ưu hóa được code trong FactoryGirl hay không.

# spec/factories/user_factory.rb

FactoryGirl.define do
  factory :user do

  # more code

  after(:create) do |user|
    create(:billing_profile, user: user)
  end
end

Trong FactoryGirl, đã tạo thêm active record cho quan hệ mà không cần sử dụng trong một số spec. Phần này có thể tối ưu thêm được.

# spec/factories/user_factory.rb

FactoryGirl.define do
  factory :user do

  # more code

  trait :with_billing_profile do
    after(:create) do |user|
      create(:billing_profile, user: user)
    end
  end
end

Như vậy bây giờ có thể tạo ra user mà có hoặc không tạo ra quan hệ thông qua việc sử dụng option :with_billing_profile Giờ thì chạy lại rspec xem tốc độ có tăng lên được chút nào không rspec4 Thật tuyệt. Tố độ đã tăng lên gần gấp 2 lần chỉ với việc sử đổi một chút để cải thiện tốc độ cho những phần test chậm nhất. Tuy nhiên việc sửa FactoryGirl như trên yêu cầu chúng ta cần phải tìm đến những chỗ cần :with_billing_profile và thêm chúng vào nhé. Đừng quên nếu không sau khi bạn sửa sẽ có vài spec bị fail test đó.

Bảo vệ chống lại việc chậm đi trong tương lai

Chúng tôi lo ngại việc FactoryGirl có thể tạo ra các record không cần thiết trong các spec bổ sung. Sử dụng Factory Girl’s documentation về ActiveSupport Instrumentation, có thể thêm logic này để thấy rõ hơn về sự cố khi cần thiết.

# spec/spec_helper.rb

config.before(:each, :monitor_database_record_creation) do |example|
  ActiveSupport::Notifications.subscribe("factory_girl.run_factory") do |name, start, finish, id, payload|
    $stderr.puts "FactoryGirl: #{payload[:strategy]}(:#{payload[:name]})"
  end
end

Bây giờ, chạy một spec với metadata :monitor_database_record_creation cho phép xác minh nếu số lượng record được tạo ra bởi bài kiểm tra là phù hợp với mong đợi. rspec5

Lợi ích của tiết kiệm thời gian chạy rspec

Bằng cách loại bỏ các global stub mà chúng tôi không cần, sử dụng chiến lược DatabaseCleaner nhanh hơn và đơn giản hoá FactoryGirl, chúng tôi đã có thể chạy rspec trong vài phút với bộ test của mình. Điều đó nghe có vẻ chẳng là gì, nhưng nhân số phút với số lượng developer trong đội và số lượng code xây dựng mỗi ngày. Thì việc đầu tư một chút thời gian để cái thiện tốc độ rspec sẽ được hoàn trả rất nhanh.