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

Thay đổi Package Name của Android Studio dể dàng với plugin APR

Nếu bạn đang gặp khó khăn hoặc bế tắc trong việc thay đổi package name trong And

Lỗi không Update Meta_Value Khi thay thế hình ảnh cũ bằng hình ảnh mới trong WordPress

Mã dưới đây hoạt động tốt có 1 lỗi không update được postmeta ” meta_key=

Bài 1 – React Native DevOps các khái niệm và các cài đặt căn bản

Hướng dẫn setup jenkins agent để bắt đầu build mobile bằng jenkins cho devloper an t

Chuyển đổi từ monolith sang microservices qua ví dụ

1. Why microservices? Microservices là kiến trúc hệ thống phần mềm hướng dịch vụ,