Một vài mẹo tăng tốc rspec
Bài đăng này đã không được cập nhật trong 6 năm
Viết test là một phần rất quan trọng trong các dự án để đảm bảo chất lượng của các đoạn code của bạn. Với các dự án nhỏ số lượng test không lớn thì tốc độ cả mỗi test không phải vấn đề gì lớn. Tuy nhiên đối với các dự án lớn thì tốc độ của rspec ảnh hưởng rất nhiều đến dự án. Trong quá trình phát triển lượng code tăng lên đồng nghĩa với lượng test cũng tăng lên. Do vậy dự án lượng lượng test sẽ rất nhiều kéo theo đó thời gian chạy rspec cũng tăng lên đáng kể. Liệu bạn có thể đợi một giờ hoặc hơn thế nữa chỉ để chạy rspec trong khi ta có thể tối ưu để nó chạy nhanh hơn?
Dưới đây tôi xin đưa ra một số mẹo giúp tăng tốc rspec
1. Tránh tạo dữ liệu trong db nhiều nhất có thể
Đa số các unit test ta có thể tạo ra object để test không nhất thiết phải tạo nó trong database
Ta có thể lấy một ví dụ sau
class User < ActiveRecord::Base
def full_name
"#{first_name} #{last_name}"
end
end
Ta viết rspec cho ví dụ trên như sau
describe User do
describe "#full_name" do
let(:user) do
FactoryGirl.create :user, first_name: "first_name",
last_name: "last_name"
end
it{expect(user.full_name).to eq "first_name last_name"}
end
end
Ta chạy thử rspec thì được kết quả sau
Finished in 2.57 seconds (files took 3.9 seconds to load)
1 example, 0 failures
Thay vì tạo ra một bản ghi trong db ta sửa lại thành chỉ build ra đối tượng như thế tốc độ sẽ cải thiện đáng kể bởi vì nó ko phải tương tác với database.
describe User do
describe "#full_name" do
let(:user) do
FactoryGirl.build :user, first_name: "first_name",
last_name: "last_name"
end
it{expect(user.full_name).to eq "first_name last_name"}
end
end
Sau khi sửa thì kết quả chạy rspec như sau
Finished in 2.3 seconds (files took 3.32 seconds to load)
1 example, 0 failures
Ta thấy với một test độ chênh lệch khoảng 0.3 giây, không lớn lắm. Tuy nhiên đây là trong trường hợp chạy một test riêng biệt kết quả sẽ chênh lệch khác nhiều trong các lần chạy khác nhau. Để kết quả chính xác hơn tôi sẽ tính tổng thời gian chạy 50 test case này
50.times{it{expect(user.full_name).to eq "first_name last_name"}}
Sau khi chạy ta được kết quả như sau
# create user
Finished in 9.08 seconds (files took 3.32 seconds to load)
50 examples, 0 failures
# build user
Finished in 3.19 seconds (files took 3.39 seconds to load)
50 examples, 0 failures
Qua ví dụ trên ta thấy có sự chênh lệch khá lớn. Trong trường hợp này với cùng một test case nếu bạn tạo dữ liệu trong db làm cho tốc độ bị chậm đi khoảng 2.8 lần (gần 3 lần)
2. Build object trực tiếp thông không dùng đến FactoryGirl (FactoryBot)
Ở phần trên ta thấy việc không tạo dữ liệu trong database đã làm tăng tốc độ cho rspec một cách đang kể. Tuy nhiên liệu dùng FactoryGirl.build liệu đã tốt? FactoryGirl rất tiện lợi giúp bạn build ra một object đầy đủ các thuộc tính. Tuy nhiên một số trường hợp ta cũng không cần phải build ra các thuộc tính thừa không cần thiết, ta có thể bỏ qua không dùng FactoryGirl trong các trường hợp này cũng là một giải pháp giúp tăng tốc rspec khá hiệu quả
Ta sửa lại rspec như sau
describe User do
describe "#full_name" do
let(:user) do
User.new :user, first_name: "first_name",
last_name: "last_name"
end
it{expect(user.full_name).to eq "first_name last_name"}
end
end
chạy thử với 50 lần ta được một kết quả cũng khá tốt (2.14 giây so với 3.19 giây của FactoryGirl.build và 9.08 giây với FactoryGirl.create)
Finished in 2.14 seconds (files took 3.41 seconds to load)
50 examples, 0 failures
3. Nên sử dụng let
thay cho let!
Sử dụng let
định nghĩa một method. Giá trị trả về của method này được cache lại cho các lần gọi khác nhau trong cùng một example nhưng đến example khác nó sẽ được khởi tạo lại. Có một điểm chú ý let
là lazy-evaluated
nó chỉ chạy khi phương thức do let
định nghĩa được gọi.
Còn let!
thì khác một chút nó định nghĩa một method đồng thời gọi method đó luôn.
Do vậy khi dùng let!
với một số test không cần sử dụng đến dữ liệu này mà nó vẫn chạy sẽ làm rspec của bạn chạy chậm hơn
4. Sử dụng before all
Với before(:each) (before) mỗi một test thì nó đều chạy lại một lần. Trong một số trường hợp để tránh chạy đi chạy lại nhằm tăng tốc cho rspec ta có thể dùng before(:all). Ta lấy một ví dụ dưới đây
class Thing
def widgets
@widgets ||= []
end
end
describe Thing do
before(:all) do
@thing = Thing.new
end
describe "initialized in before(:all)" do
it "has 0 widgets" do
@thing.should have(0).widgets
end
it "can get accept new widgets" do
@thing.widgets << Object.new
end
it "shares state across examples" do
@thing.should have(1).widgets
end
end
end
Trong ví dụ trên @thing
sẽ chỉ được khởi tạo một lần khi chạy qua cả 3 lần test. Đây là một ví dụ đơn giản với một số trường hợp phức tạp xử lí nhiều mà phải lặp đi lặp lại cũng khá mất thời gian.
5. Sử dụng mock và stubs
Trong một số trường hợp để test một method A trong nội dung method A lại gọi đến method B. Việc test hoàn chỉnh method A với cả phần kết quả trả về của method B là khó và việc khởi tạo dữ liệu trong trường hợp này cũng rất phức tạp và tốn thời gian. Thay vì vậy ta có thể stubs lại method B cho nó trả về một giá trị nào đó rồi test method A dựa trên giá trị đã cho ở trên. Như vậy việc test cũng đơn giản hơn nhiều và lượng dữ liệu khởi tạo cũng không quá lớn.
6. Không tự động build ra các associations không cần thiết
Việc tạo kèm các associations là khá mất thời gian hơn nữa một vài trường hợp sẽ tạo ra một dãy các quan hệ phụ thuộc trong khi test của bạn không cần thiết phải tạo ra đầy đủ hết dữ liệu như vậy
factory :authorization
user { create :user }
end
Trong ví dụ trên một số trường hợp authorization không cần thiết phải tạo ra user thì ta không nên tạo user kèm với authorization. Ta có thể sửa lại như sau
factory :authorization
trait :with_user do
user { create :user }
end
end
Khi nào cần tạo ra authorization
có user ta sẽ gọi build :authorization, :with_user
All rights reserved