Mẹo để tăng tốc unit test ruby
Bài đăng này đã không được cập nhật trong 7 năm
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).
All rights reserved