Testing is a crucial part of ensuring the reliability and correctness of a Ruby on Rails 8 application. Controller tests verify the behaviour of your application’s controllers, ensuring that actions handle requests properly, return correct responses, and enforce security measures.
This guide explores the best practices in writing Rails 8 controller tests, references well-known Rails projects, and provides 20 test case examplesโincluding 5 complex ones.
Setting Up the Testing Environment using Rspec
To effectively write controller tests, we use RSpec (the most popular testing framework in the Rails community) along with key supporting gems:
Recommended Gems
Add the following gems to your Gemfile under the :test group:
group :test do
gem 'rspec-rails' # Main testing framework
gem 'factory_bot_rails' # For test data setup
gem 'database_cleaner-active_record' # Cleans test database
gem 'faker' # Generates fake data
gem 'shoulda-matchers' # Provides one-liner matchers for common Rails functions
end
Run:
bundle install
rails generate rspec:install
Then, configure spec_helper.rb and rails_helper.rb to include necessary test configurations.
Types of Controller Tests
A controller test should cover various scenarios:
- Successful actions (index, show, create, update, destroy)
- Error handling (record not found, invalid params)
- Authentication & Authorization (user roles, access control)
- Redirections & Response types (HTML, JSON, Turbo Streams)
- Edge cases (empty parameters, SQL injection attempts)
Let’s dive into examples.
Basic Controller Tests
1. Testing Index Action
require 'rails_helper'
describe ArticlesController, type: :controller do
describe 'GET #index' do
it 'returns a successful response' do
get :index
expect(response).to have_http_status(:ok)
end
end
end
2. Testing Show Action with a Valid ID
describe 'GET #show' do
let(:article) { create(:article) }
it 'returns the requested article' do
get :show, params: { id: article.id }
expect(response).to have_http_status(:ok)
expect(assigns(:article)).to eq(article)
end
end
3. Testing Show Action with an Invalid ID
describe 'GET #show' do
it 'returns a 404 for an invalid ID' do
get :show, params: { id: 9999 }
expect(response).to have_http_status(:not_found)
end
end
4. Testing Create Action with Valid Parameters
describe 'POST #create' do
it 'creates a new article' do
expect {
post :create, params: { article: attributes_for(:article) }
}.to change(Article, :count).by(1)
end
end
5. Testing Create Action with Invalid Parameters
describe 'POST #create' do
it 'does not create an article with invalid parameters' do
expect {
post :create, params: { article: { title: '' } }
}.not_to change(Article, :count)
end
end
6. Testing Update Action
describe 'PATCH #update' do
let(:article) { create(:article) }
it 'updates an article' do
patch :update, params: { id: article.id, article: { title: 'Updated' } }
expect(article.reload.title).to eq('Updated')
end
end
7. Testing Destroy Action
describe 'DELETE #destroy' do
let!(:article) { create(:article) }
it 'deletes an article' do
expect {
delete :destroy, params: { id: article.id }
}.to change(Article, :count).by(-1)
end
end
Here are the missing test cases (7 to 15) that should be included in your blog post:
8. Testing Redirection After Create
describe 'POST #create' do
it 'redirects to the article show page' do
post :create, params: { article: attributes_for(:article) }
expect(response).to redirect_to(assigns(:article))
end
end
9. Testing JSON Response for Index Action
describe 'GET #index' do
it 'returns a JSON response' do
get :index, format: :json
expect(response.content_type).to eq('application/json')
end
end
10. Testing JSON Response for Show Action
describe 'GET #show' do
let(:article) { create(:article) }
it 'returns the article in JSON format' do
get :show, params: { id: article.id }, format: :json
expect(response.content_type).to eq('application/json')
expect(response.body).to include(article.title)
end
end
11. Testing Unauthorized Access to Update
describe 'PATCH #update' do
let(:article) { create(:article) }
it 'returns a 401 if user is not authorized' do
patch :update, params: { id: article.id, article: { title: 'Updated' } }
expect(response).to have_http_status(:unauthorized)
end
end
12. Testing Strong Parameters Enforcement
describe 'POST #create' do
it 'does not allow mass assignment of protected attributes' do
expect {
post :create, params: { article: { title: 'Valid', admin_only_field: true } }
}.to raise_error(ActiveModel::ForbiddenAttributesError)
end
end
13. Testing Destroy Action with Invalid ID
describe 'DELETE #destroy' do
it 'returns a 404 when the article does not exist' do
delete :destroy, params: { id: 9999 }
expect(response).to have_http_status(:not_found)
end
end
14. Testing Session Persistence
describe 'GET #dashboard' do
before { session[:user_id] = create(:user).id }
it 'allows access to the dashboard' do
get :dashboard
expect(response).to have_http_status(:ok)
end
end
15. Testing Rate Limiting on API Requests
describe 'GET #index' do
before do
10.times { get :index }
end
it 'returns a 429 Too Many Requests when rate limit is exceeded' do
get :index
expect(response).to have_http_status(:too_many_requests)
end
end
Complex Controller ๐ฎ Tests
16. Testing Admin Access Control
describe 'GET #admin_dashboard' do
context 'when user is admin' do
let(:admin) { create(:user, role: :admin) }
before { sign_in admin }
it 'allows access' do
get :admin_dashboard
expect(response).to have_http_status(:ok)
end
end
context 'when user is not admin' do
let(:user) { create(:user, role: :user) }
before { sign_in user }
it 'redirects to home' do
get :admin_dashboard
expect(response).to redirect_to(root_path)
end
end
end
17. Testing Turbo Stream Responses
describe 'PATCH #update' do
let(:article) { create(:article) }
it 'updates an article and responds with Turbo Stream' do
patch :update, params: { id: article.id, article: { title: 'Updated' } }, format: :turbo_stream
expect(response.media_type).to eq Mime[:turbo_stream]
end
end
Here are three additional complex test cases (18, 19, and 20) to include in your blog post:
18. Testing WebSockets with ActionCable
describe 'WebSocket Connection' do
let(:user) { create(:user) }
before do
sign_in user
end
it 'successfully subscribes to a channel' do
subscribe room_id: 1
expect(subscription).to be_confirmed
expect(subscription).to have_stream_from("chat_1")
end
end
Why? This test ensures that ActionCable properly subscribes users to real-time chat channels.
19. Testing Nested Resource Actions
describe 'POST #create in nested resource' do
let(:user) { create(:user) }
let(:post) { create(:post, user: user) }
it 'creates a comment under the correct post' do
expect {
post :create, params: { post_id: post.id, comment: { body: 'Nice post!' } }
}.to change(post.comments, :count).by(1)
end
end
Why? This test ensures correct behavior when working with nested resources like comments under posts.
20. Testing Multi-Step Form Submission
describe 'PATCH #update (multi-step form)' do
let(:user) { create(:user, step: 'personal_info') }
it 'advances the user to the next step in a multi-step form' do
patch :update, params: { id: user.id, user: { step: 'address_info' } }
expect(user.reload.step).to eq('address_info')
end
end
Why? This test ensures users can progress through a multi-step form properly.
๐ Conclusion
This guide provides an extensive overview of controller testing in Rails 8, ensuring robust coverage for all possible scenarios. By following these patterns, your Rails applications will have reliable, well-tested controllers that behave as expected.
Happy Testing! ๐
One thought on “Rails 8 App: Comprehensive Guide ๐ to Write Controller Tests | ๐ Rspec – 20 Test Cases For Reference”