9 tip để cải thiện khả năng maintain RSpec

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

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à:

  1. DRY — Don’t Repeat Yourself principle
  2. 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

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ