70 phút
Testing với RSpec
Setup
# Gemfile
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'faker'
gem 'shoulda-matchers'
end
group :test do
gem 'capybara'
gem 'selenium-webdriver'
gem 'database_cleaner-active_record'
end
bundle install
rails generate rspec:install
Model Specs
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'validations' do
it { should validate_presence_of(:name) }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email).case_insensitive }
it { should have_secure_password }
end
describe 'associations' do
it { should have_many(:posts).dependent(:destroy) }
end
describe '#admin?' do
it 'returns true for admin users' do
user = build(:user, role: 'admin')
expect(user.admin?).to be true
end
it 'returns false for regular users' do
user = build(:user, role: 'user')
expect(user.admin?).to be false
end
end
describe '#email' do
it 'downcases email before save' do
user = create(:user, email: 'JOHN@EXAMPLE.COM')
expect(user.email).to eq('john@example.com')
end
end
end
Factories
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.unique.email }
password { 'password123' }
role { 'user' }
trait :admin do
role { 'admin' }
end
trait :with_posts do
transient do
posts_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
end
end
end
end
# spec/factories/posts.rb
FactoryBot.define do
factory :post do
title { Faker::Lorem.sentence }
body { Faker::Lorem.paragraphs(number: 3).join("\n\n") }
published { true }
association :user
trait :draft do
published { false }
end
end
end
Controller/Request Specs
# spec/requests/posts_spec.rb
require 'rails_helper'
RSpec.describe 'Posts', type: :request do
let(:user) { create(:user) }
let(:post_record) { create(:post, user: user) }
describe 'GET /posts' do
it 'returns success' do
get posts_path
expect(response).to have_http_status(:success)
end
end
describe 'GET /posts/:id' do
it 'shows the post' do
get post_path(post_record)
expect(response).to have_http_status(:success)
expect(response.body).to include(post_record.title)
end
end
describe 'POST /posts' do
context 'when signed in' do
before { sign_in(user) }
it 'creates a new post' do
expect {
post posts_path, params: {
post: { title: 'New', body: 'Body', published: true }
}
}.to change(Post, :count).by(1)
expect(response).to redirect_to(post_path(Post.last))
end
it 'fails with invalid params' do
expect {
post posts_path, params: { post: { title: '' } }
}.not_to change(Post, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'when not signed in' do
it 'redirects to login' do
post posts_path, params: { post: { title: 'New', body: 'Body' } }
expect(response).to redirect_to(login_path)
end
end
end
end
Authentication helper
# spec/support/request_helpers.rb
module RequestHelpers
def sign_in(user)
post login_path, params: { email: user.email, password: 'password123' }
end
end
RSpec.configure do |config|
config.include RequestHelpers, type: :request
end
Feature Specs (System Tests)
# spec/system/user_registration_spec.rb
require 'rails_helper'
RSpec.describe 'User registration', type: :system do
before { driven_by(:rack_test) }
it 'allows a user to register' do
visit signup_path
fill_in 'Name', with: 'John Doe'
fill_in 'Email', with: 'john@example.com'
fill_in 'Password', with: 'password123'
fill_in 'Password confirmation', with: 'password123'
click_button 'Sign up'
expect(page).to have_content 'Welcome'
expect(User.last.email).to eq('john@example.com')
end
it 'shows validation errors' do
visit signup_path
click_button 'Sign up'
expect(page).to have_content "Name can't be blank"
end
end
# spec/system/post_management_spec.rb
RSpec.describe 'Post management', type: :system do
let(:user) { create(:user) }
before do
driven_by(:rack_test)
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'password123'
click_button 'Log in'
end
it 'creates a new post' do
visit new_post_path
fill_in 'Title', with: 'My First Post'
fill_in 'Body', with: 'This is the content'
check 'Published'
click_button 'Create Post'
expect(page).to have_content 'Post was successfully created'
expect(page).to have_content 'My First Post'
end
end
Policy Specs
# spec/policies/post_policy_spec.rb
require 'rails_helper'
RSpec.describe PostPolicy do
subject { described_class }
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:post) { create(:post, user: user) }
permissions :show? do
it 'allows owner' do
expect(subject).to permit(user, post)
end
it 'allows admin' do
expect(subject).to permit(admin, post)
end
it 'denies other users for unpublished' do
draft = create(:post, :draft, user: user)
expect(subject).not_to permit(other_user, draft)
end
end
permissions :update?, :destroy? do
it 'allows owner' do
expect(subject).to permit(user, post)
end
it 'allows admin' do
expect(subject).to permit(admin, post)
end
it 'denies other users' do
expect(subject).not_to permit(other_user, post)
end
end
end
Job Specs
# spec/jobs/send_welcome_email_job_spec.rb
require 'rails_helper'
RSpec.describe SendWelcomeEmailJob, type: :job do
include ActiveJob::TestHelper
let(:user) { create(:user) }
it 'queues the job' do
expect {
described_class.perform_later(user.id)
}.to have_enqueued_job(described_class).with(user.id)
end
it 'sends an email' do
expect {
described_class.perform_now(user.id)
}.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
Test Configuration
# spec/rails_helper.rb
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
abort e.to_s.strip
end
RSpec.configure do |config|
config.fixture_paths = ["#{::Rails.root}/spec/fixtures"]
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
# FactoryBot
config.include FactoryBot::Syntax::Methods
# Devise test helpers
# config.include Devise::Test::IntegrationHelpers, type: :request
end
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
Running Tests
bundle exec rspec
bundle exec rspec spec/models
bundle exec rspec spec/models/user_spec.rb
bundle exec rspec spec/models/user_spec.rb:15 # specific line
# Coverage
COVERAGE=true bundle exec rspec
Bài tập thực hành
Hãy viết tests cho blog app!