Skip
Arish's avatar

49. Testing Basics


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:system

Model 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
21end

Controller 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
51end

Fixtures

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: false

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'
11end
bash
1rails generate rspec:install

Running 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 documentation

Model 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
32end

Request 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
48end

Factories

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
27end

System 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
23end

Testing is essential for building robust Rails applications!