Mẹo để tăng tốc unit test ruby

Câu chuyện về Unit test của chúng ta sẽ không đáng phải bàn với các project nhỏ, khi mà số lượng unit test là ít và thời gian chạy unit test RSpec chỉ trong khoảng một vài phút trở lại. Tuy nhiên, nếu project của chúng ta trở nên lớn hơn, và số lượng unit test sẽ rất lớn, thời gian kiểm tra các unit test có thể lớn hơn 10 phút, và bộ full test sẽ chạy kiểm tra trong khoảng 20 phút (hoặc lớn hơn) thì là cả một vấn đề. Như vậy là quá lâu. Vậy làm thế nào để cải thiện tình trạng này, khi mà số lượng unit test quá lớn? Sau đây là một số đề xuất của tôi.

Các vấn đề gặp phải và cách phòng tránh

build vs build_stubbed vs create

Khi nhìn vào test logs của chúng ta, chúng ta chợt nhận ra có quá nhiều SQL queries được tạo ra. Chúng ta đang sử dụng FactoryGirl chẳng hạn, và chúng ta đã quá thường xuyên create một record. Việc tương tác với database tiêu tốn rất nhiều thời gian. Trong file configuration RSpec, chúng ta setup như sau:

RSpec.configure do |config|
 config.before(:suite) do
    # transaction is advised for ActiveRecord users, but you can also use :truncation 
    # and :deletion (these are database_cleaner strategies 
    # (cf. https://github.com/DatabaseCleaner/database_cleaner)
   DatabaseCleaner[:active_record].clean_with(:transaction)
 end

 # Our queries are made inside a transaction, rolled-back after each example
 config.around(:each) do |example|
   DatabaseCleaner.cleaning do
     example.run
   end
 end
end

Đấy là lý do tại sao chúng ta hãy dùng build or build_stubbed bất cứ khi nào có thể. Theo như document thì với một record user trong unit test của chúng ta:

# Returns a User instance that’s not saved
user = build(:user)
# Returns an object with all defined attributes stubbed out
stub = build_stubbed(:user)
# Returns a saved User instance
user = create(:user)

Rõ ràng, việc dùng create đã tạo ra một biến instance user và nó được lưu lại. Thay vào đó, việc chúng ta sử dụng build or build_stubbed (nếu được) thì chỉ tạo ra instance và nó không được lưu, việc này giảm được thời gian tương tác với database và tiêu tốn bộ nhớ. build_stubbed cũng gần giống như build, cũng tạo ra các thuộc tính và assign các attributes giống như build, nhưng đó là những điểm giống nhau ở khi kết thúc. Nó tạo ra các objects như đã được hợp lệ, tạo ra các associations với build_stubbed (trong khi build vẫn sử dụng create), và đưa ra một số methods tương tác với database và sẽ raise nếu bạn gọi đến chúng. Điều này làm cho bộ test của bạn nhanh hơn và giảm sự phụ thuộc của bộ test vào database. Bạn hãy tham khảo thêm về stubs Use Factory Girl's build_stubbed for a Faster Test Suite

Bạn nên thử áp dụng cách này với những unit test mà không yêu cầu model insert vào database test của bạn, trong những trường hợp như vậy sẽ nhanh hơn rất nhiều.

before(:each) vs before(:all)

before(:example) 
# aka before(:each), run before each example
before(:context) 
# aka before(:all), run one time only, before all of the examples in a group

before là cách tốt nhất để thiết lập trước cho nhiều đoạn test, nhưng những thiết lập của bạn đặc biệt nặng, ví dụ như rất nhiều models được tạo ra, bạn nên cân nhắc chỉ thực hiện chúng một lần, cho tất cả assertions của bạn. Bởi mặc định before nghĩa là before(:each). Bạn thử tưởng tượng mà xem, với mỗi lần chạy test bạn lại tương tác với database rất nhiều thì bộ test sẽ trở nên rất nặng nề đấy.

Loại bỏ tất cả let! không cần thiết

Trong RSpec bạn có thể dùng let Sử dụng let để định nghĩa một helper method có tính ghi nhớ. Giá trị sẽ được lưu trữ trong nhiều lần gọi trong cùng một example chứ không phải qua nhiều example.

Lưu ý rằng giá trị của biến được định nghĩa bằng let có nghĩa sau khi biến ấy được gọi thực thi lần đầu tiên. Bạn có thể sử dụng let! để buộc các phương thức gọi trước mỗi example test. Đôi khi việc sử dụng let bên trong before(:all) sẽ là tốt hơn với tất cả các requirements cần để tránh gọi nhiều lần gọi. Hãy thử loại bỏ tất cả let! không cần thiết và refactoring lại test của bạn xem, nó sẽ tránh được việc bắt buộc phải gọi một biến trước mỗi example.

Chạy song song tests

Michael Grosser đã tạo ra gem Parallel test dựa trên parallel. ParallelTests chia các tests ra thành các group test (theo số dòng hoặc thời gian chạy) và chạy mỗi nhóm test như là một single process với database riêng của nó. Tuy nhiên sẽ là không khả thi nếu trong code của bạn có những lỗi được gọi là sinh ra ngẫu nhiên (classes không được loaded, HTTP calls không thể stubbed…)

Các factories associations vô dụng

Nếu chúng ta tạo ra quá nhiều records với FactoryGirl:

factory :authorization do |auth|
  auth.user { create :user }
end

factory :user do |user|
  user.role { create :role }
  user.organisation { create :organisation }
end

Khi chạy trên console bạn sẽ nhận ra ngay vấn đề:

=> require 'factory_girl'
=> FactoryGirl.find_definitions
=> time = Time.now; FactoryGirl.create(:authorization); puts "Time spent : #{Time.now - time} s"
   (0.2ms)  BEGIN
  SQL (0.4ms)  INSERT INTO "organisations"
   (5.4ms)  COMMIT
   (0.2ms)  BEGIN
  SQL (2.7ms)  INSERT INTO "users"
   (1.5ms)  COMMIT
   (0.2ms)  BEGIN
  SQL (0.4ms)  INSERT INTO "authorizations"   
    (1.3ms)  COMMIT

=> Time spent : 0.121693 s

Mất quá nhiều thời gian để tương tác với database. Bỏ qua các associations này, và biến chúng thành những thuộc tính:

factory :authorization do |auth|
  auth.trait :with_user do |auth|
    auth.user { create :user }
  end
end

Việc loại bỏ các associations này tiết kiệm được khá nhiều thời gian do không phải tạo những object mới, mà chúng ta có thể linh hoạt loại bỏ bớt đi những associations không thật sự cần thiết. Bạn có thể dùng gem Factory Doctor, gem này có thể đưa ra thông báo khi bạn tạo ra những data vô dụng trong tests. Tuy rằng gem này có vẻ không hoàn toàn perfect nhưng nó vẫn có thể đưa ra được những thông báo về các associations vô dụng hoặc là những create không cần thiết.

Kết luận

Việc tăng tốc cho unit test đem lại nhiều lợi ích cho dev như là việc không bị ức chế khi phải chời đợi thời gian chạy test quá lâu, tăng tốc độ phát triển dự án, đồng thời cũng giúp cho việc maintain bớt nhàm chán và khó khăn hơn bằng cách loại bỏ đi một mớ thừa làm cho người maintain bớt cảm thấy rối rắm và dễ hiểu code hơn. Tuy nhiên, phải nắm được khi nào thật sự cần thiết và loại bỏ được những tác nhân gây thừa và chậm tốc độ test như create hay let!, before chứ không nhất nhất là phải loại bỏ hoàn toàn dẫn đến sai về mặt logic (yaoming).