Testing in Rails
Testing is a fundamental part of Rails development. Rails comes with Minitest built-in, but many developers prefer RSpec. We'll cover both.
Why Test?
- Catch bugs before production
- Enable confident refactoring
- Document how code should work
- Prevent regressions
- Speed up development long-term
Minitest (Built-in)
Running Tests
bash
1# Run all tests
2rails test
3
4# Run specific file
5rails test test/models/user_test.rb
6
7# Run specific test
8rails test test/models/user_test.rb:15
9
10# Run model tests
11rails test:models
12
13# Run controller tests
14rails test:controllers
15
16# Run system tests
17rails test:systemModel Tests
ruby
1# test/models/user_test.rb
2require "test_helper"
3
4class UserTest < ActiveSupport::TestCase
5 test "should not save user without email" do
6 user = User.new(password: "password123")
7 assert_not user.save, "Saved user without email"
8 end
9
10 test "should save user with valid attributes" do
11 user = User.new(email: "test@example.com", password: "password123")
12 assert user.save
13 end
14
15 test "email should be unique" do
16 User.create!(email: "test@example.com", password: "password")
17 user = User.new(email: "test@example.com", password: "password")
18 assert_not user.valid?
19 assert_includes user.errors[:email], "has already been taken"
20 end
21endController Tests
ruby
1# test/controllers/articles_controller_test.rb
2require "test_helper"
3
4class ArticlesControllerTest < ActionDispatch::IntegrationTest
5 setup do
6 @article = articles(:one)
7 @user = users(:one)
8 end
9
10 test "should get index" do
11 get articles_url
12 assert_response :success
13 end
14
15 test "should get new when logged in" do
16 sign_in @user
17 get new_article_url
18 assert_response :success
19 end
20
21 test "should create article" do
22 sign_in @user
23 assert_difference("Article.count") do
24 post articles_url, params: {
25 article: { title: "New Article", body: "Content" }
26 }
27 end
28 assert_redirected_to article_url(Article.last)
29 end
30
31 test "should show article" do
32 get article_url(@article)
33 assert_response :success
34 end
35
36 test "should update article" do
37 sign_in @user
38 patch article_url(@article), params: {
39 article: { title: "Updated Title" }
40 }
41 assert_redirected_to article_url(@article)
42 end
43
44 test "should destroy article" do
45 sign_in @user
46 assert_difference("Article.count", -1) do
47 delete article_url(@article)
48 end
49 assert_redirected_to articles_url
50 end
51endFixtures
yaml
1# test/fixtures/users.yml
2one:
3 email: john@example.com
4 password_digest: <%= BCrypt::Password.create('password') %>
5
6two:
7 email: jane@example.com
8 password_digest: <%= BCrypt::Password.create('password') %>
9
10# test/fixtures/articles.yml
11one:
12 title: First Article
13 body: This is the first article.
14 user: one
15 published: true
16
17two:
18 title: Second Article
19 body: This is the second article.
20 user: two
21 published: falseRSpec (Popular Alternative)
Setup
ruby
1# Gemfile
2group :development, :test do
3 gem 'rspec-rails'
4 gem 'factory_bot_rails'
5 gem 'faker'
6end
7
8group :test do
9 gem 'shoulda-matchers'
10 gem 'database_cleaner-active_record'
11endbash
1rails generate rspec:installRunning Specs
bash
1# Run all specs
2bundle exec rspec
3
4# Run specific file
5bundle exec rspec spec/models/user_spec.rb
6
7# Run specific test
8bundle exec rspec spec/models/user_spec.rb:15
9
10# Run with format
11bundle exec rspec --format documentationModel Specs
ruby
1# spec/models/user_spec.rb
2require 'rails_helper'
3
4RSpec.describe User, type: :model do
5 describe 'validations' do
6 it { should validate_presence_of(:email) }
7 it { should validate_uniqueness_of(:email).case_insensitive }
8 it { should validate_length_of(:password).is_at_least(8) }
9 end
10
11 describe 'associations' do
12 it { should have_many(:articles).dependent(:destroy) }
13 it { should have_one(:profile) }
14 end
15
16 describe '#full_name' do
17 it 'returns first and last name' do
18 user = build(:user, first_name: 'John', last_name: 'Doe')
19 expect(user.full_name).to eq('John Doe')
20 end
21 end
22
23 describe '.active' do
24 it 'returns only active users' do
25 active_user = create(:user, active: true)
26 inactive_user = create(:user, active: false)
27
28 expect(User.active).to include(active_user)
29 expect(User.active).not_to include(inactive_user)
30 end
31 end
32endRequest Specs
ruby
1# spec/requests/articles_spec.rb
2require 'rails_helper'
3
4RSpec.describe "Articles", type: :request do
5 let(:user) { create(:user) }
6 let(:article) { create(:article, user: user) }
7
8 describe "GET /articles" do
9 it "returns a list of articles" do
10 create_list(:article, 3)
11
12 get articles_path
13
14 expect(response).to have_http_status(:success)
15 expect(response.body).to include("Articles")
16 end
17 end
18
19 describe "POST /articles" do
20 context "when logged in" do
21 before { sign_in user }
22
23 it "creates a new article" do
24 expect {
25 post articles_path, params: {
26 article: { title: "New", body: "Content" }
27 }
28 }.to change(Article, :count).by(1)
29
30 expect(response).to redirect_to(article_path(Article.last))
31 end
32
33 it "fails with invalid params" do
34 post articles_path, params: { article: { title: "" } }
35
36 expect(response).to have_http_status(:unprocessable_entity)
37 end
38 end
39
40 context "when not logged in" do
41 it "redirects to login" do
42 post articles_path, params: { article: { title: "New" } }
43
44 expect(response).to redirect_to(new_user_session_path)
45 end
46 end
47 end
48endFactories
ruby
1# spec/factories/users.rb
2FactoryBot.define do
3 factory :user do
4 email { Faker::Internet.email }
5 password { 'password123' }
6 first_name { Faker::Name.first_name }
7 last_name { Faker::Name.last_name }
8
9 trait :admin do
10 role { 'admin' }
11 end
12
13 trait :with_articles do
14 after(:create) do |user|
15 create_list(:article, 3, user: user)
16 end
17 end
18 end
19end
20
21# spec/factories/articles.rb
22FactoryBot.define do
23 factory :article do
24 title { Faker::Lorem.sentence }
25 body { Faker::Lorem.paragraphs(number: 3).join("\n\n") }
26 published { true }
27 user
28
29 trait :draft do
30 published { false }
31 end
32 end
33end
34
35# Usage
36user = create(:user)
37admin = create(:user, :admin)
38user_with_articles = create(:user, :with_articles)
39article = create(:article, user: user)
40draft = create(:article, :draft)Configuration
ruby
1# spec/rails_helper.rb
2require 'spec_helper'
3require 'rspec/rails'
4
5RSpec.configure do |config|
6 config.include FactoryBot::Syntax::Methods
7 config.include Devise::Test::IntegrationHelpers, type: :request
8
9 config.before(:suite) do
10 DatabaseCleaner.strategy = :transaction
11 DatabaseCleaner.clean_with(:truncation)
12 end
13
14 config.around(:each) do |example|
15 DatabaseCleaner.cleaning do
16 example.run
17 end
18 end
19end
20
21# Shoulda Matchers config
22Shoulda::Matchers.configure do |config|
23 config.integrate do |with|
24 with.test_framework :rspec
25 with.library :rails
26 end
27endSystem Tests
ruby
1# spec/system/articles_spec.rb
2require 'rails_helper'
3
4RSpec.describe "Articles", type: :system do
5 let(:user) { create(:user) }
6
7 before do
8 driven_by(:selenium_chrome_headless)
9 end
10
11 it "allows user to create an article" do
12 sign_in user
13
14 visit new_article_path
15
16 fill_in "Title", with: "My New Article"
17 fill_in "Body", with: "This is the content"
18 click_button "Create Article"
19
20 expect(page).to have_content("Article was successfully created")
21 expect(page).to have_content("My New Article")
22 end
23endTesting is essential for building robust Rails applications!
