+1

[RubyonRails][Rspec] Unit Test trong rails (Phần 2)

Trong phần trước mình đã giới thiệu sương sương về Rspec trong Rails và một số gem hỗ trợ, và trong bài viết này mình sẽ nói nhiều hơn về những thành phần cơ bản khi viết Rspec

Sử dụng describe , context, it

Trong RSpec, describe, context, và it là các phương thức được sử dụng để viết và tổ chức các test case một cách rõ ràng và dễ đọc.

  • describe: describe được sử dụng để nhóm các test case lại với nhau dưới một tiêu đề chung, thường là để mô tả một đối tượng, một class, hoặc một phương thức đang được kiểm tra. Nó giúp tổ chức test suite của bạn thành các phần nhỏ, dễ quản lý và hiểu.

    describe "Tên đối tượng hoặc phần của mã nguồn cần kiểm tra" do
      # Các test case liên quan đến đối tượng hoặc phần mã nguồn này sẽ được định nghĩa ở đây
    end
    
  • context: context được sử dụng để tạo ra một bối cảnh hoặc điều kiện cụ thể cho các test case. Thường được sử dụng để mô tả các trạng thái hoặc điều kiện khác nhau mà đối tượng cần được kiểm tra.

    context "Khi điều kiện hoặc trạng thái nhất định xảy ra" do
     # Các test case liên quan đến điều kiện hoặc trạng thái này sẽ được định nghĩa ở đây
    end
    
  • it: it được sử dụng để mô tả một test case cụ thể, trong đó bạn định nghĩa điều kiện cần kiểm tra và kết quả mong đợi của test case đó.

    it "Mô tả điều kiện cần kiểm tra" do
      # Mã kiểm tra và kết quả mong đợi sẽ được định nghĩa ở đây
    end
    

Và thứ tự của 3 thằng này theo chuẩn của nó là describe -> context -> it

describe 'User entity testing ' do
  context 'when creating a new user' do
    it 'is valid with valid attributes' do
      # Kiểm tra tính hợp lệ của user khi tạo mới
    end

    it 'is not valid without a name' do
      # Kiểm tra user không hợp lệ nếu thiếu thuộc tính name
    end
  end

  context 'when updating an existing user' do
    it 'updates the user attributes' do
      # Kiểm tra cập nhật thuộc tính của user thành công
    end

    it 'does not allow invalid attributes' do
      # Kiểm tra user không được cập nhật nếu dữ liệu mới không hợp lệ
    end
  end
end

let , let!, beforesubject

Trong quá trình viết RSpec có một số phương thức thường sử dụng để thiết lập trạng thái ban đầu và các biến trong các test case và làm cho code Rspec của bạn trở nên dễ hiểu hơn!

  • let: let được sử dụng để định nghĩa một biến instance trong toàn bộ phạm vi của một describe hoặc một context. Biến được tạo bởi let chỉ được khởi tạo khi nó được gọi đến lần đầu tiên trong mỗi test case.

    describe User do
      let(:user) { User.new(name: 'John', email: 'john@example.com') }
    
      it 'has a name' do
        expect(user.name).to eq('John')
      end
    
      it 'has an email' do
        expect(user.email).to eq('john@example.com')
      end
    end
    
  • let!: let! tương tự như let, nhưng nó sẽ tạo và khởi tạo biến ngay khi một describe hoặc một context được gọi. Điều này có nghĩa là biến được tạo bởi let! sẽ được khởi tạo trước mỗi test case.

    describe User do
      let!(:user) { User.create(name: 'John', email: 'john@example.com') }
    
      it 'has a name' do
        expect(user.name).to eq('John')
      end
    
      it 'has an email' do
        expect(user.email).to eq('john@example.com')
      end
    end
    
    
  • before: before được sử dụng để thiết lập trạng thái ban đầu hoặc thực hiện các hành động trước khi mỗi test case chạy. Nó thường được sử dụng để thực hiện các thao tác chuẩn bị dữ liệu trước khi test case được thực hiện.

    describe User do
      before do
        @user = User.new(name: 'John', email: 'john@example.com')
      end
    
      it 'has a name' do
        expect(@user.name).to eq('John')
      end
    
      it 'has an email' do
        expect(@user.email).to eq('john@example.com')
      end
    end
    
  • subject: subject được sử dụng để định nghĩa một biến instance mặc định mà được sử dụng trong các test case. Khi không có subject được gọi, RSpec sẽ sử dụng biến mặc định của subject là biến subject.

    subject trong RSpec là một cơ chế mạnh mẽ cho phép bạn định nghĩa một biến hoặc một đối tượng mặc định để sử dụng trong các test case của một describe hoặc một context. subject giúp làm cho code test của bạn trở nên ngắn gọn và dễ đọc hơn bằng cách loại bỏ sự lặp lại và làm tăng tính rõ ràng của mã.

    1. Định nghĩa subject cơ bản
      
      describe User do
        subject { User.new(name: 'John', email: 'john@example.com') }
      
        it 'has a name' do
          expect(subject.name).to eq('John')
        end
      
        it 'has an email' do
          expect(subject.email).to eq('john@example.com')
        end
      end
      

      Trong ví dụ này, subject được định nghĩa là một đối tượng User mới với các thuộc tính được chỉ định. Trong mỗi test case, chúng ta sử dụng subject để tham chiếu đến đối tượng này.

    2. Sử dụng subject với biến mặc định
      

      Nếu không có subject được định nghĩa trong một describe, RSpec sẽ tự động tạo ra một subject mặc định từ tên của describe.

      describe User do
        it 'has a name' do
          expect(subject.name).to eq('John')
        end
      
        it 'has an email' do
          expect(subject.email).to eq('john@example.com')
        end
      end
      

      Trong trường hợp này, subject sẽ trở thành một đối tượng User mới tự động được tạo ra bởi RSpec.

    3. Sử dụng subject với biến đặc biệt
      

      Bạn có thể đặt tên cho subject và sử dụng nó trong toàn bộ describe hoặc context.

      describe User do
        subject(:custom_user) { User.new(name: 'Jane', email: 'jane@example.com') }
      
        it 'has a name' do
          expect(custom_user.name).to eq('Jane')
        end
      
        it 'has an email' do
          expect(custom_user.email).to eq('jane@example.com')
        end
      end
      

      Trong trường hợp này, custom_user được định nghĩa là một biến subject với tên là custom_user, và bạn có thể sử dụng nó trong các test case trong describe.

      Một ví dụ khác khi dùng subject. Giả sử bạn đang viết Rspec cho controller và đang viết testcase cho phương thức Create với method (Post) thì bạn hoàn toàn có thể viết như sau:

      describe 'POST #create' do
          subject(:action) { post user_path, params: params }
      
          context 'mô tả context 1' do
             # thay đổi params linh hoạt để phù hợp với testcase 
            let(:params) { { user_atrributes: valid_params } }
      
            it 'Mô tả điều kiện cần kiểm tra' do
              action
              expect(response).to ...
            end
          end
      
          context 'mô tả context 2' do
            # thay đổi params linh hoạt để phù hợp với testcase 
            let(:params) { { user_atrributes: valid_params } }
      
            it 'Mô tả điều kiện cần kiểm tra' do
              expect ...
            end
      
            it 'Mô tả điều kiện cần kiểm tra' do
              action
              expect(response).to ...
            end
          end
      
          context 'mô tả context 3' do
            # thay đổi params linh hoạt để phù hợp với testcase 
            let(:params) { { user_atrributes: invalid_params } }
      
            it 'Mô tả điều kiện cần kiểm tra' do
              expect ...
            end
      
            it 'Mô tả điều kiện cần kiểm tra' do
              action
              expect(response).to ...
            end
          end
        end
      

Mocking và Stubbing

MOCKSTUBS là hai kỹ thuật quan trọng trong unit testing để giả lập hoặc thay thế các thành phần phụ thuộc (dependencies) của một phần của mã nguồn mà bạn đang kiểm tra. Điều này giúp bạn kiểm tra chức năng của một phần mã nguồn mà không phụ thuộc vào hành vi hoặc trạng thái của các thành phần khác

Giả sử bạn có 1 class Calculator và có phương thức add

class Calculator
  def add(a, b)
    return a + b
  end
end
  • Stubbing

    allow(some_object).to receive(some_method).and_return(some_value)

    Trong stubbing, chúng ta giả lập hoặc thay thế một phần của mã nguồn để kiểm tra hành vi của mã nguồn khác. Trong ví dụ này, chúng ta sẽ sử dụng stubbing để thay thế phương thức add của class Calculator.

    require 'calculator'
     RSpec.describe Calculator do
       describe '#add' do
         it 'adds two numbers' do
           calculator = Calculator.new
           allow(calculator).to receive(:add).and_return(5)
    
           result = calculator.add(2, 3)
    
           expect(result).to eq(5)
         end
       end
     end
    

    Ở đây, chúng ta sử dụng phương thức allow của RSpec để mock phương thức add của đối tượng calculator và trả về giá trị 5. Điều này giả lập việc thực thi của phương thức add mà không cần dùng tới implement thực tế của nó.

  • Mocking

     `expect(some_object).to receive(some_method).and_return(some_value)`
    

    Về cơ bản thì Mocking và Stubbing khá giống nhau chỉ khác nhau ở chỗ:

    1. Mock thể hiện những sự mong đợi khi gọi đến một method nào đó, nó có thể là kết quả trả về, là đối số truyền vào method đó hay là số lần gọi đến method đó.
    2. Trong khi đó Stub chỉ như là định nghĩa/cho phép một object gọi đến một method và trả về một kết quả nào đó

    Ngoài allowexpect chúng ta còn có các phương thức hay dùng, các bạn tham khảo tại đây

Sử dụng Factory Bot hiệu quả

  • Cơ bản về Factory Bot

    Factory Bot là một thư viện giúp tạo ra dữ liệu mẫu cho các test case một cách linh hoạt và dễ dàng. Bằng cách sử dụng Factory Bot, bạn có thể tạo ra các đối tượng và dữ liệu liên quan một cách tự động, giúp rút ngắn thời gian viết test case và tạo ra các trường hợp test phong phú.

  • Nâng cao với Factory Bot

    1. Sử dụng Traits

      Traits là một tính năng mạnh mẽ của Factory Bot cho phép bạn định nghĩa các thuộc tính và hành vi đặc biệt cho các factory. Điều này giúp tạo ra các phiên bản của cùng một đối tượng với các thuộc tính khác nhau dễ dàng hơn.

    2. Kết hợp với Faker

      Faker là một thư viện giúp tạo ra dữ liệu giả mạo một cách tự động. Kết hợp Factory Bot với Faker giúp bạn tạo ra các dữ liệu thực tế và phong phú hơn cho các test case của mình.

  • Ví dụ về Sử dụng Factory Bot

    Giả sử bạn có một model User trong ứng dụng của mình và bạn muốn viết test case cho nó bằng cách sử dụng Factory Bot. Dưới đây là một ví dụ về cách sử dụng Factory Bot để tạo ra các đối tượng User trong test case:

    # spec/factories/users.rb
       FactoryBot.define do
         factory :user do
           name { Faker::Name.name }
           email { Faker::Internet.email }
           password { Faker::Internet.password }
    
           trait :admin do
             admin { true }
           end
         end
       end
    

    Trong ví dụ trên, chúng ta định nghĩa một factory cho đối tượng User với các thuộc tính name, email và password được tạo ra bằng cách sử dụng Faker. Ngoài ra, chúng ta cũng định nghĩa một trait là :admin để tạo ra các user có quyền admin.

    Sau đó, bạn có thể sử dụng factory này trong test case của mình như sau:

    # spec/models/user_spec.rb
    
        require 'rails_helper'
    
        RSpec.describe User, type: :model do
          it 'is valid with valid attributes' do
            user = FactoryBot.create(:user)
            expect(user).to be_valid
          end
    
          it 'is an admin' do
            admin_user = FactoryBot.create(:user, :admin)
            expect(admin_user.admin).to eq(true)
          end
        end
    

Kết luận

Trên đây là những hiểu biết của mình về cách thực hiện unit testing trong Rails sử dụng RSpec. Hi vọng những chia sẻ ở trên sẽ giúp ích được cho bạn đọc khi bắt đầu dự án Ruby on Rails của mình.

Tài liệu tham khảo

https://rubydoc.info/gems/rspec-mocks/frames

https://www.martinfowler.com/articles/mocksArentStubs.html


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí