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 subject
và describe
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 viewnew.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 mock và stub.
- 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 nhanh và rõ 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é!