Bốn cách để cải thiện và tăng tốc độ khi viết RSpec test
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 ...
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/