Song song với việc viết code, thì để đảm bảo code đó đảm bảo về mặt logic, đúng spec hơn, ít lỗi hơn và có khả năng maintain hơn thì việc viết unit test là một việc quan trong không hề kém, việc này đôi khi quan trọng như là viết code vậy. Với Ruby on Rails khi code thì việc viết Rspec cũng là công việc song hành, vậy làm sao để viết Rspec cũng dễ dàng maintain có tính mở rộng thì sau đây mình sẽ đưa ra 9 tips bạn đọc có thể xem có thể áp dụng với dự án hiện tại của mình không nhé.
Có 2 nguyên tắc để xây dựng nên những tip này, đó là:
- DRY — Don’t Repeat Yourself principle
- Use the right tool at the right place for the right purpose
1. Cấu trúc code đúng vào vị trí của nó
Thường thì có 3 khối cơ bản cho các test case đó là
- Setup — before, let, let!
- Assert — it
- Teardown — after
Code phải được cấu trúc phù hợp thành các block phù hợp.
# BAD#
describe '#sync'do
it 'updates the local account balance'do
local_account.open
transfer(1000)
expect { local_account.sync }.to change { local_account.balance }.by(1000)
widthdraw_all
local_account.close
endend# GOOD#
describe '#sync'do
subject { local_account.sync }
before do# Setup
local_account.open
transfer(1000)end
after do# Teardown
widthdraw_all
local_account.close
end
it 'updates the local account balance'do# Assert
expect { subject }.to change { local_account.balance }.by(1000)endend
2. Hạn chế mock global classes/modules/objects
Global classes/modules/objects có xu hướng được sử dụng ở nhiều nơi nằm ngoài scope test hiện tại. Mocking những thành phần đó sẽ vi phạm nguyên tắc isolation principle của unit testing, điều này sẽ dẫn đến tác dụng phụ.
Quy tắc này đặc biệt đúng khi mock phương thức new
của các class.
# BAD#classUserService
attr_reader :userdefverify_email# ...
email_service =EmailService.new(user)
email_service.send_confirmation_email
# ...endend
describe UserServicedo
describe '#verify_email'do
before do
email_service = double(:email_service)# BAD: Mocking `new` method of EmailService
allow(EmailService).to receive(:new).and_return(email_service)
allow(email_service).to receive(:send_confirmation_email)endendend# GOOD#classUserService
attr_reader :userdefverify_email
email_service = generate_email_service
email_service.send_confirmation_email
endprivate# You can also use memoization if ONLY 1 instance of EmailService is neededdefgenerate_email_serviceEmailService.new(user)endend
describe UserServicedo
describe '#verify_email'do
before do
email_service = double(:email_service)# GOOD: Mocking its own method `generate_email_service`
allow(described_class).to receive(:generate_email_service).and_return(email_service)
allow(email_service).to receive(:send_confirmation_email)endendend
3. Sử dụng instance_double thay vì double
Khi bạn muốn tạo một mock instance của một class, instance_double là một lựa chọn an toàn hơn. Khác với double, instance_double sẽ đưa ra các exceptions nếu các mocked behaviors được thực hiện dưới dạng các instance method của provided class. Điều này cho phép chúng ta nắm bắt các vấn đề sâu hơn so với việc sử dụng double.
classFootballPlayerdefshoot# ...shoot...endend
messi = instance_double(FootballPlayer)
allow(messi).to receive(:shoot)# OK
allow(messi).to receive(:shoot).with('power')# Wrong numbers of arguments
allow(messi).to receive(:score)# Player does not implement: score
ronaldo = double('FootballPlayer')
allow(ronaldo).to receive(:shoot)# OK
allow(ronaldo).to receive(:shoot).with('power')# OK - but silent failure
allow(ronaldo).to receive(:score)# OK - but silent failure
4. Sử dụng DESCRIBE cho testing targets và CONTEXT cho các tình huống(scenarios)
Nó chỉ là một cách để làm cho code của mình nghe trôi chảy hơn.
describe UserStoredo
describe '.create'do
context 'when user does not exists'do
it 'creates a new user'do# ...end
describe 'the newly created user'do
it 'has the correct attributes'do# ...endendend
context 'when user already exists'do
it 'raises error'do# ...endendendend
5. Viết code implement DESCRIBE và CONTEXT ngay bên dưới statement
Điều này rất quan trọng để đảm bảo các test được thiết lập theo các described contexts. Điều này cũng giúp phân biệt context này với context khác.
describe 'FootballPlayer'do
let(:speed){50}
let(:shooting){50}
let(:player)do
create(:football_player,
speed: speed,
shooting: shooting,)end
describe '#position'do
subject { player.position }
context 'when the player is fast'do
let(:speed){98}# implements 'when the user is fast'
it { is_expected.to eq 'winger'}end
context 'when the player shoots well'do
let(:shooting){90}# implements 'when the player shoots well'
it { is_expected.to eq 'striker'}end
context 'when the player is injured'do
before { player.injure }# implements `when the player is injured`
it { is_expected.to eq 'benched'}
context 'when the player uses doping'do# both injured and using doping
before { player.use_doping }
it { is_expected.to eq 'midfielder'}endendendend
6. Sử dụng bulk mothods nếu có thể
# BAD
it 'has correct attributes'do
expect(user.name).to eq 'john'
expect(user.age).to eq 20
expect(user.email).to eq '[email protected]'
expect(user.gender).to eq 'male'
expect(user.country).to eq 'us'end# GOOD
it 'has correct attributes'do
expect(user).to have_attributes(
name:'john',
age:20,
email:'[email protected]',
gender:'male',
country:'us',)end
7. Hiểu cách transactions hoạt động trong RSpec
Theo mặc định, các transaction được tạo và bao quanh mỗi example. Điều này cho phép tất cả các database operation bên trong một example được roll back để đảm bảo một clean slate cho example tiếp theo.
Việc tạo database record bên trong các hooks nhất định như trước (: context) hoặc trước (: all) sẽ không được khôi phục bởi các transactions mặc định đã đề cập ở trên. Điều này sẽ dẫn đến dữ liệu bị cũ.
context 'context 1'do
before(:context)do
create(:user)# WON'T BE ROLLED-BACKend
before do
create(:user)# will be rolled-backend# ...end
context 'context 2'do
before(:context)do
create(:user)# WON'T BE ROLLED-BACKend# ...end# BY NOW, THERE ARE 2 USER RECORDS COMMITED TO DATABASE
8. Hạn chế sử dụng expect cho mocking
Mặc dù expect
có thể được sử dụng cho mục đích mocking, expect là command chính thức cho các assertion.
Còn allow
là công cụ chính xác để mock.
Hơn nữa, chúng ta hãy tự nhắc mình rằng mocking là một phần của giai settup test, KHÔNG phải giai đoạn assertion .
# BAD: expect...and_return
it 'returns the sync value'do
expect(service).to receive(:sync).and_return(value)# mix between setup and assertion
expect(subject).to eq value
end# GOOD
before do
allow(service).to receive(:sync).and_return(value)# Set upend
describe 'the service'do
it 'syncs'do
expect(service).to receive(:sync)# assertendend
it { is_expected.to eq value }# assert
9. Sử dụng configs cho các test case dạng khuôn mẫu.
Nó sẽ DRY và dễ dàng hơn cho người đọc theo dõi.
# BAD#
describe '.extract_extension'do
subject { described_class.extract_extension(filename)}
context 'when the filename is empty'do
let(:filename){''}
it { is_expected.to eq ''}end
context 'when the filename is video123.mp4'do
let(:filename){'video123.mp4'}
it { is_expected.to eq 'mp4'}end
context 'when the filename is video.edited.mp4'do
let(:filename){'video.edited.mp4'}
it { is_expected.to eq 'mp4'}end
context 'when the filename is video-edited'do
let(:filename){'video-edited'}
it { is_expected.to eq ''}end
context 'when the filename is .mp4'do
let(:filename){'.mp4'}
it { is_expected.to eq ''}endend# GOOD#
describe '.extract_extension'do
subject { described_class.extract_extension(filename)}
test_cases =[''=>'','video123.mp4'=>'mp4''video.edited.mp4'=>'mp4''video-edited'=>'''.mp4'=>'']
test_cases.eachdo|test_filename, extension|
context "when filename = #{test_filename}"do
let(:filename){ test_filename }
it { is_expected.to eq extension }endendend
Kết
Đây là một số thứ tôi đã đúc kết ra khi làm việc với rspec mong bạn đọc có thể áp dụng nó chút nào đó vào dự án của mình.
Bài viết được tham khảo từ: 9 tips to improve RSpec maintainability
Nguồn: viblo.asia