Bốn cách để cải thiện và tăng tốc độ khi viết RSpec test
Bài đăng này đã không được cập nhật trong 6 năm
Tests là một phần rất quan trọng trong mỗi ứng dụng, tuy nhiên, thi thoảng sẽ rất khó để giữ cho bộ test nhanh và rõ ràng, đặc biệt khi có nhiều lập trình viên từng làm trước hoặc đang làm cùng bạn trong dự án. Trong bài viết này, mình tập trung vào việc cải thiện RSpec tests bằng 2 cách: cấu trúc lại những đoạn test bị lặp lại nhiều lần và tăng tốc độ test bằng cách giảm request tới database.
Shared examples
Hãy xem xét ví dụ sau đây:
class AgePolicy
def old_enough?(age)
age >= 18
end
end
Đây là một hàm đơn giản để giúp chúng ta kiểm tra xem người dùng đã đủ tuổi để thực hiện 1 hành động gì đó trong ứng dụng hay chưa. Chúng ta có một đoạn test vô cùng đơn giản cho class này:
require 'spec_helper'
describe AgePolicy do
describe '#old_enough?' do
it 'returns false if user is 16 years old' do
policy = AgePolicy.new
expect(policy.old_enough?(16)).to eq(false)
end
it 'returns false if user is 12 years old' do
policy = AgePolicy.new
expect(policy.old_enough?(12)).to eq(false)
end
it 'returns true if user is 18 years old' do
policy = AgePolicy.new
expect(policy.old_enough?(18)).to eq(true)
end
it 'returns true if user is 20 years old' do
policy = AgePolicy.new
expect(policy.old_enough?(20)).to eq(true)
end
end
end
Như bạn thấy, điều duy nhất chúng ta test là tuổi. Đây là một trường hợp mà ta có thể dùng shared_examples
được cung cấp bởi framework RSpec
require 'spec_helper'
describe AgePolicy do
describe '#old_enough?' do
shared_examples 'user eligible for taking an action' do |age|
it "returns true if user is #{age} years old" do
policy = AgePolicy.new
expect(policy.old_enough?(age)).to eq(true)
end
end
shared_examples 'user not eligible for taking an action' do |age|
it "returns false if user is #{age} years old" do
policy = AgePolicy.new
expect(policy.old_enough?(age)).to eq(false)
end
end
it_behaves_like 'user not eligible for taking an action', 16
it_behaves_like 'user not eligible for taking an action', 12
it_behaves_like 'user eligible for taking an action', 18
it_behaves_like 'user eligible for taking an action', 20
end
end
Mặc dù giờ nhìn hơi khó đọc hơn, nhưng chúng ta sẽ tránh được việc lặp code trong mỗi ví dụ, test cũng sẽ nhanh hơn 1 chút.
Custom matchers
RSpec có rất nhiều matcher
hữu dụng. Thỉnh thoảng, có những giá trị mà chúng ta expect
được lặp đi lặp lại trong code. Một ví dụ rất tốt cho trường hợp này là khi ta test response của controller. Hãy xem xét ví dụ dưới đây
class SomeController
def show
render json: { success: true }
end
end
Giờ làm sao để test nếu nó trả về thuộc tính success
với giá trị true
? Nó sẽ như thế này
describe SomeController do
describe 'GET #show' do
it 'returns success response' do
get :show, id: 11, format: :json
expect(JSON.parse(response.body)).to eq({success: true})
end
end
end
Sẽ đơn giản hơn nhiều nếu như chỉ viết thế này
expect(response).to be_json_success
Để tạo ra một matcher
như vậy, bạn tạo file matchers.rb
(tên tùy ý) trong thư mục spec/support
và mô tả matcher
của bạn trong đó:
RSpec::Matchers.define :be_json_success do |expected|
match do |actual|
json_response = JSON.parse(actual.body)
expect(json_response['success']).to eq(true)
end
end
Bước cuối cùng là thêm dòng require 'support/matchers'
trong file spec_helper.rb
Loại bỏ các association không cần thiết khỏi factories
Giả sử bạn có model User
và mỗi user có một bản ghi Contact
và Location
. Factory của bạn trông sẽ tương tự như vậy:
FactoryGirl.define do
factory :user do
contact
location
end
end
Mỗi lần bạn sử dụng FactoryGirl.create :user
hay FactoryGirl.build :user
, sẽ đồng thời có thêm 2 bản ghi nữa được tạo ra, kể cả có dùng build
. Nếu bạn chỉ muốn tạo ra user mà không cần bất kì bản ghi association
nào, bạn nên sử dụng FactoryGirl.build_stubbed :user
thay thế.
Giờ hãy thử tưởng tượng bản sử dụng 1 factory cho mọi case test. Nếu bạn thật sự luôn cần cả associations
thì không vấn đề gì, nhưng nếu thỉnh thoảng mới cần đến associations
thì có thể xem xét sử dụng traits
để tăng tốc độ của test:
FactoryGirl.define do
factory :user do
first_name { "John" }
last_name { "Doe" }
trait :with_location do
location
end
end
end
Và bạn sử dụng bằng cách gọi FactoryGirl.create :user, :with_location
.
Sử dụng Rails transactional để test feature
Bao nhiêu lần bạn chỉ sử dụng 1 bản ghi cho mọi case test? Hãy sử dụng lại factory user
ở phần trên để viết 1 test đơn giản:
require 'spec_helper'
describe User do
let!(:user) { FactoryGirl.create :user }
it 'does something' do
# test with user
end
it 'does something' do
# test with user
end
it 'does something' do
# test with user
end
end
Bao nhiêu bản ghi user
được tạo trong cơ sở dữ liệu? Một bản ghi cho mỗi example
vì vậy câu trả lời là 3. Để tránh việc này bạn sử dụng helper let_it_be
của gem test-prof. Helper này sử dụng Rails transactional tests feature
, có nghĩa là nó sẽ tạo ra duy nhất 1 bản ghi khi bắt đầu case test và xóa đi khi test xong. Nếu gem đã được cài đặt, bạn phải thêm require 'test_prof/recipes/rspec/let_it_be'
vào spec_helper.rb
và helper đã sẵn sàng sử dụng.
require 'spec_helper'
describe User do
let_it_be(:user) { FactoryGirl.create :user }
it 'does something' do
# test with user
end
it 'does something' do
# test with user
end
it 'does something' do
# test with user
end
end
Nếu như bạn dùng cùng 1 bản ghi cho nhiều example
thì nó thậm chí có thể giúp bạn tăng tốc độ lên 50%. Đừng quên bạn cũng có thể sử dụng stubs
nếu không thực sự cần thiết phải lấy dữ liệu từ database.
Nguồn: http://pdabrowski.com/blog/ruby-on-rails/testing/4-ways-to-refactor-and-speed-up-tests/
All rights reserved