Một số thủ thuật làm tăng tốc Rspec test

Một số thủ thuật làm tăng tốc Rspec test

Lí do cần test phải chạy nhanh

Developer cần test chạy nhanh để có thể nhanh chóng kiểm tra xem code của mình chạy đúng hay không, từ đó có thể phát hiện ra bug một cách nhanh nhất. Nếu test chạy chậm thì dev sẽ thường rất ngại chạy và dần dần họ sẽ bỏ luôn viết test và chạy test.

Một trong những lí do khiến test chạy chậm đó là việc khởi động (boot) test ban đầu chậm. Điều này sẽ ảnh hưởng cả khi bạn chỉ chạy duy nhất 1 test đơn giản (không gọi api, không tương tác với DB). Ta cùng tìm hiểu một số cách khắc phục.

Hạn chế việc require gem trong ứng dụng

Thông thường với ứng dụng rails thì những gì có trong Gemfile đều sẽ được load vào ứng dụng của chúng ta mỗi khi nó được khởi động để làm bất kì điều gì. Để hạn chế việc load toàn bộ gem vào ứng dụng thì ta có thể sử dụng một số kỹ thuật như bỏ require mặc định của gem đó hay nhóm mỗi gem cho từng môi trường khác nhau.

Ví dụ:

Giả sử ta có 1 Gemfile như sau:

gem "rails"
gem "bcrypt"
gem "foreman"
gem "thin"
gem "newrelic_rpm"
gem "airbrake"

Đầu tiên, ta có thể ngừng việc load một số gem không cần sử dụng trong code của chúng ta theo mặc định:

gem "rails"
gem "bcrypt"
gem "foreman", require: false
gem "thin", require: false
gem "newrelic_rpm"
gem "airbrake"

Sau đó ta có thể nhóm các gem vào từng môi trường nó cần được dùng đến:

gem "rails"
gem "bcrypt"

group :development do
  gem "foreman", require: false
end

group :production do
  gem "thin", require: false
  gem "newrelic_rpm"
  gem "airbrake"
end

Giảm thời gian chạy setting test

Setting test là những thứ cần được thiết lập trước khi chạy test. Nó đơn giản có thể là require một class để có thể gọi được trong test hay thực hiện việc xóa DB mỗi lần chạy test xong.

Để có thể giảm được thời gian chạy setting test thì ta có thể áp dụng một số cách sau:

  1. Đưa các đoạn setup code cho test vào riêng biệt mỗi test, hay nói cách khác là mỗi test sẽ có các setup độc lập với nhau, test nào cần thứ gì thì require trực tiếp thứ đó.

    Ví dụ khi test file app/models/user.rb thì trong test ta require trực tiếp file này vào để test.

    Ưu điểm của phương pháp này là tốc độ load test sẽ nhanh do chỉ phải load một phần nhỏ các file cần thiết cho test. Nhưng ngược lại thì việc viết test sẽ tốn thòi gian hơn vì phải tốn thêm thời gian để viết mannual những đoạn code setup test (khá nhàm chán và lặp lại)

    Trong Rspec, những gì cần được setting để test chạy được thực hiện hết trong spec_helper.rbrails_helper.

    Thông thường thì toàn bộ code và gem trong ứng dụng của bạn sẽ được load hết trong các file setup trên. Ngoài ra nó cũng là nơi mà ta setup các đoạn code hỗ trợ cho test của chúng ta như việc xóa DB, mock api call... Ta có thể tối ưu việc setup này thông qua việc tạo nhóm.

    Ví dụ:

    Ta có một config như sau trong spec_helper.rb:

    config.before(:each) do
      stub_something
    end
    

    Config này sẽ luôn chạy stub_something trước mọi tập test. Nhìn qua thôi đã thấy là nó chạy rất nhiều rồi. Với trường hợp mà test không thực sự cần đến stub_something thì việc này vô cùng thừa thãi và là tác nhân làm test chạy chậm. Ta có thể đổi lại như sau:

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

    Như vậy, stub_something sẽ chỉ chạy với những test nào có :stub_something mà thôi:

    scenario "user does something", :stub_something do
      # spec logic
    end
    
  2. Setup toàn bộ những gì cần thiết cho test và tìm cách cache lại

    Đây là cách mà ta vẫn hay làm trong các dự án rails. spec_helperrails_helper sẽ load toàn bộ dependencies của ứng dụng và sau đó ta dùng spring để cache lại toàn bộ việc load này. Hay đối với integration test, khi mà ta phải chọc vào DB để test. Ta có thể sử dụng một cách là dump toàn bộ các bảng trong DB ra một file để khi test thì ta sẽ load một lần cùng với việc sử dụng fixture để tạo dữ liệu test 1 lần duy nhất.

    Với cách này thì ta không phải viết nhiều code để setup test và tốc độ chạy test sẽ được đảm bảo chạy ổn định hơn. Vấn đề duy nhất là lần load đầu tiên sẽ rất tốn thời gian và sẽ tốn resource để cache lại toàn bộ setting test.

Factories hoặc fixtures

Factories được dùng để có thể tạo dữ liệu trong DB tại bất kì chỗ nào trong code và tại bất kì thời điểm nào. Điều này làm chúng linh hoạt hơn, dễ tái sử dụng hơn và có thể đảm bảo được dữ liệu độc lập cho từng test. Điểm yếu của chúng là việc chạy độc lập với nhau, mỗi Factories tạo record trong một transaction độc lập. Mỗi lần tạo dữ liệu nó sẽ tạo một connection đến DB, và như vậy tốc độ của nó chậm là điều tất yếu.

Fixture ngược lại sẽ tạo record theo dạng bulk. Toàn bộ record sẽ được tạo trong 1 transaction. 1 transaction sẽ được tạo khi 1 test được chạy. Sau khi test được chạy thì DB sẽ được roll back về trạng thái ban đầu. Điều này cho thấy là Fixture không cần phải load lại mỗi khi chạy test.

Hầu hết các Factories sẽ bắt đầu một test với một DB rỗng và chạy tất cả các Factories cần cho 1 test mỗi khi test đó được chạy. Sau khi test chạy xong thì DB được reset về trạng thái rỗng bằng cách xóa toàn bộ dữ liệu được tạo ra trước đấy. Ngoài ra thì Factories sẽ sử dụng các ActiveRecord model để thực hiện việc tạo dữ liệu này, nó sẽ tạo ra nhiều ruby object và do đó làm tốn thêm resource của hệ thống.

Do vậy, cố gắng sử dụng Fixture khi nào có thể sẽ giúp tăng tốc độ của test.

Phát hiện điểm gây nên test chậm trong Rspec

Trong Rspec, ta có thể sử dụng profile flag để hiển thị các spec chậm nhất trong dự án:

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

Lệnh trên sẽ list 5 spec chạy chậm nhất trong file spec/models/user_spec.rb cho chúng ta.

Ngoài ra, bạn có thể config để kiểm tra xem có factories nào được tạo thừa hay không khi test được chạy như sau:

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

Khi chạy test với :monitor_database_record_creation, ta sẽ biết được số lượng record được tạo ra trong test với factory có phù hợp so với mong muốn của ta hay không.

References

Rails speed up tests

Debugging slowed down test