+2

10 Mẹo Viết Test Trong Rails Nhanh – Gọn – Hiệu Quả

Viết test trong Rails không chỉ để “cho có”, mà còn giúp chúng ta tự tin khi refactor, thêm tính năng mới hay fix bug. Tuy nhiên, nếu viết test sai cách, chúng ta sẽ rơi vào bẫy: test chậm, khó đọc, dễ vỡ. Cùng điểm qua 10 mẹo giúp viết test Rails đúng và gọn nhé!

1. Sử dụng rails generate để tạo test file chuẩn

Khi bạn tạo model, controller hay bất kỳ thành phần nào trong Rails, hãy luôn dùng lệnh rails generate. Lệnh này không chỉ tạo ra file code chính mà còn tự động tạo file test tương ứng với cấu trúc chuẩn.

rails generate model Post title:string

# Lệnh này sẽ tạo ra:
# app/models/post.rb
# test/models/post_test.rb  # <-- File test chuẩn

👉 Việc này giúp bạn giảm thiểu lỗi thiếu file test, đảm bảo tuân thủ cấu trúc project chuẩn của Rails và có sẵn sườn để bắt đầu viết test ngay lập tức.

2. Ưu tiên dùng RSpec nếu project lớn

Rails mặc định đi kèm với Minitest, khá đơn giản và hiệu quả cho các project nhỏ. Tuy nhiên, nếu bạn đang xây dựng một project lớn với nhiều logic nghiệp vụ phức tạp, RSpec là một lựa chọn phổ biến và mạnh mẽ hơn.

RSpec mang lại cú pháp BDD (Behavior-Driven Development) thân thiện, giúp bạn viết test theo cách mô tả hành vi của ứng dụng:

describe Post do
  it "is valid with a title" do
    post = Post.new(title: "Bài viết của tôi")
    expect(post).to be_valid
  end

  it "is invalid without a title" do
    post = Post.new(content: "Nội dung bài viết")
    expect(post).not_to be_valid
    expect(post.errors[:title]).to include("can't be blank")
  end
end

👉 RSpec dễ viết, dễ đọc, có cộng đồng lớn và nhiều gem hỗ trợ, giúp bạn quản lý test suite lớn tốt hơn.

3. Sử dụng FactoryBot để tạo dữ liệu test gọn

Thay vì tạo dữ liệu test bằng cách gọi trực tiếp User.create(...), hãy dùng FactoryBot. Nó giúp bạn định nghĩa các "nhà máy" (factories) để tạo ra các đối tượng (object) với dữ liệu mẫu một cách nhất quán và dễ tái sử dụng.

FactoryBot.define do
  factory :user do
    sequence(:name) { |n| "User #{n}" } # Tạo tên duy nhất
    sequence(:email) { |n| "user#{n}@example.com" } # Tạo email duy nhất
    password { "password123" }
    password_confirmation { "password123" }
  end

  factory :admin, parent: :user do
    admin { true }
  end
end

Trong file test:

# Sử dụng FactoryBot trong test
let(:user) { create(:user) }        # Tạo và lưu user vào DB
let(:admin_user) { create(:admin) } # Tạo admin user

👉 Cách này giúp code test của bạn gọn hơn nhiều, dễ đọc và dễ quản lý dữ liệu test

4. Tránh dùng create nếu không cần truy cập DB

Trong khi create của FactoryBot tạo và lưu đối tượng vào database, thì build chỉ tạo đối tượng trong bộ nhớ mà không tương tác với database.

let(:new_user) { build(:user) } # Chỉ tạo user trong bộ nhớ

Sử dụng build khi bạn chỉ muốn test các validation hoặc phương thức không yêu cầu đối tượng phải tồn tại trong database.

describe User do
  it "is valid with a name and email" do
    user = build(:user) # Không cần lưu vào DB để kiểm tra tính hợp lệ
    expect(user).to be_valid
  end

  it "is invalid without an email" do
    user = build(:user, email: nil)
    expect(user).not_to be_valid
  end
end

👉 Việc này giúp test chạy nhanh hơn rất nhiều vì tránh được các thao tác đọc/ghi vào database không cần thiết.

5. Dùng subjectdescribe rõ ràng

Trong RSpec, subject cho phép bạn định nghĩa đối tượng đang được test trong một ngữ cảnh cụ thể, giúp cú pháp test trở nên gọn gàng và dễ đọc hơn, đặc biệt khi bạn muốn test một phương thức cụ thể.

describe '#full_name' do # Test phương thức full_name của User
  let(:user) { build(:user, name: "name", email: "email@example.com") } # Cần một user có tên

  subject { user.full_name } # Định nghĩa subject là kết quả của user.full_name

  it { is_expected.to eq "fullname" } # Kiểm tra subject có đúng bằng "name" không
end

👉 Cách này tăng tính rõ ràng cho test và giúp bạn tránh lặp lại tên phương thức cần test.

6. Dùng context để nhóm các trường hợp

context trong RSpec cho phép bạn nhóm các test case có cùng điều kiện hoặc cùng một ngữ cảnh. Điều này giúp test suite của bạn có cấu trúc rõ ràng, dễ đọc và dễ bảo trì hơn rất nhiều.

describe Post do
  let(:post) { create(:post) }

  describe '#publish' do
    context 'when user is admin' do
      let(:admin_user) { create(:admin) }

      it 'publishes the post successfully' do
        expect(post.publish(admin_user)).to be_truthy
        expect(post).to be_published
      end
    end

    context 'when user is guest' do
      let(:guest_user) { create(:user) }

      it 'raises an error about permissions' do
        expect { post.publish(guest_user) }.to raise_error(Pundit::NotAuthorizedError) # Ví dụ
        expect(post).not_to be_published
      end
    end
  end
end

👉 Test dễ đọc, dễ bảo trì hơn vì bạn biết chính xác điều kiện nào đang được kiểm tra.

7. Không test những thứ của Rails

Đây là một nguyên tắc quan trọng: chỉ test logic riêng của ứng dụng của bạn. Rails và các thư viện bên thứ ba đã được test kỹ lưỡng bởi những nhà phát triển của chúng.

Ví dụ:

  • Không cần test has_many :posts trong Model, vì đó là chức năng core của ActiveRecord.
  • Không cần test liệu validates :title, presence: true có hoạt động hay không.
  • Không cần test liệu render :new có hiển thị đúng view new.html.erb hay không.

👉 Tập trung sức lực vào việc test logic và các phương thức tùy chỉnh mà bạn tự viết. Điều này giúp test chạy nhanh hơn và tập trung vào những phần dễ phát sinh lỗi nhất trong ứng dụng của bạn.

8. Mock và Stub khi test Controller / Service

Khi test Controller hoặc Service Object có tương tác với các thành phần khác (như Model, API bên ngoài, Mailer), bạn nên sử dụng mockstub.

  • Mock: Tạo một đối tượng giả mạo (mock object) để mô phỏng hành vi của đối tượng thật.
  • Stub: Thay thế một phương thức của đối tượng thật bằng một giá trị trả về giả lập.
describe PostsController, type: :controller do
  let(:post) { instance_double(Post, id: 1, title: "Test Post") } # Tạo một mock Post

  before do
    # Stub Post.find để nó không truy cập DB, mà trả về mock_post
    allow(Post).to receive(:find).and_return(post)
  end

  describe "GET #show" do
    it "finds the post by id" do
      get :show, params: { id: post.id }
      expect(Post).to have_received(:find).with(post.id.to_s)
      expect(assigns(:post)).to eq(post)
    end
  end
end

👉 Việc này giúp giảm phụ thuộc vào database, giúp test chạy nhanhrõ ràng hơn. Nó cũng cho phép bạn test các kịch bản khó tái tạo trong môi trường thật (ví dụ: API trả về lỗi).

9. Viết test ngắn gọn, rõ mục tiêu

Mỗi test case (khối it trong RSpec) nên kiểm tra đúng một thứ và có một mục tiêu rõ ràng. Tránh viết các test case quá dài, kiểm tra quá nhiều thứ cùng lúc.

# Bad: Một test kiểm tra quá nhiều thứ, khó debug khi fail
it "should create a post and check all attributes and status and user" do
  # quá nhiều logic trong một test
  # ... kiểm tra đủ thứ
end

# Good: Mỗi test kiểm tra một hành vi cụ thể
it "saves the post to the database" do
  expect { post.save }.to change(Post, :count).by(1)
end

it "associates the post with the current user" do
  post.save
  expect(post.user).to eq(current_user)
end

it "sets the post status to 'draft' by default" do
  post.save
  expect(post.status).to eq('draft')
end

👉 Test ngắn gọn giúp bạn dễ dàng xác định nguyên nhân lỗi khi một test case thất bại.

10. Chạy test nhanh bằng --only-failures hoặc --next-failure

Khi bạn đang trong quá trình phát triển hoặc fix bug, việc chạy toàn bộ test suite có thể tốn thời gian. Rails và RSpec cung cấp các tùy chọn để chạy test hiệu quả hơn:

  • rspec --only-failures: Chỉ chạy lại những test case đã thất bại ở lần chạy trước. RSpec tự động lưu trạng thái các test case bị lỗi.
  • rspec --next-failure: Dừng việc chạy test ngay sau khi tìm thấy test case đầu tiên bị lỗi. Hữu ích khi bạn muốn khắc phục lỗi ngay lập tức mà không cần chờ toàn bộ suite chạy xong.
  • rspec spec/models/post_spec.rb: Chạy test cho một file cụ thể.
  • rspec spec/models/post_spec.rb:10: Chạy test cho một test case cụ thể ở dòng số 10.

👉 Các lệnh này giúp bạn tập trung fix lỗi nhanh chóng mà không cần tốn thời gian chạy toàn bộ test suite.

Kết luận

Viết test tốt là một kỹ năng quan trọng đối với bất kỳ nhà phát triển Rails nào. Nó không chỉ đơn thuần là để "đảm bảo đúng" mà còn là một cách tư duy và thiết kế phần mềm chắc chắn hơn. Bằng cách áp dụng những mẹo trên, bạn sẽ có thể viết các test chạy nhanh, dễ đọc, dễ bảo trì và mang lại sự tự tin khi phát triển các hệ thống lớn.

Hãy đầu tư thời gian vào việc viết test từ đầu, và bạn sẽ thấy giá trị mà nó mang lại khi cần refactor, thêm tính năng mới hoặc xử lý bug trong các dự án của mình.

Bạn thường dùng công cụ nào để viết test trong Rails và có mẹo nào hay ho muốn chia sẻ không? Hãy để lại bình luận phía dưới nhé!


All rights reserved

Bình luận

Đang tải thêm bình luận...
Avatar
+2
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í