All posts

Testing Error Scenarios in Rails Applications

Write effective Rails error scenario tests with RSpec, factory patterns, controller error tests, and background job failure verification.

Testing Error Scenarios in Rails Applications

Rails' testing ecosystem makes error scenario testing straightforward. Here's how to cover failure paths with RSpec.

Controller Error Tests

RSpec.describe Api::V1::UsersController, type: :request do
  describe 'GET /api/v1/users/:id' do
    it 'returns 404 when user not found' do
      get '/api/v1/users/nonexistent'

      expect(response).to have_http_status(:not_found)
      expect(json_response['error']).to include('not found')
    end

    it 'returns 401 when not authenticated' do
      get '/api/v1/users/1'

      expect(response).to have_http_status(:unauthorized)
    end
  end

  describe 'POST /api/v1/users' do
    it 'returns 422 with validation errors' do
      post '/api/v1/users', params: { user: { email: 'invalid' } },
        headers: auth_headers

      expect(response).to have_http_status(:unprocessable_entity)
      expect(json_response['details']).to include(/Email is invalid/)
    end
  end
end

Model Validation Tests

RSpec.describe Order, type: :model do
  describe 'validations' do
    it 'rejects negative quantities' do
      order = build(:order, quantity: -1)
      expect(order).not_to be_valid
      expect(order.errors[:quantity]).to include('must be greater than 0')
    end

    it 'enforces uniqueness of order number' do
      create(:order, order_number: 'ORD-001')
      duplicate = build(:order, order_number: 'ORD-001')
      expect(duplicate).not_to be_valid
    end
  end
end

Service Object Error Tests

RSpec.describe CreateOrder do
  it 'handles payment gateway timeout' do
    allow(PaymentGateway).to receive(:charge)
      .and_raise(Net::OpenTimeout)

    result = described_class.new.call(valid_params)

    expect(result.success).to be false
    expect(result.error).to include('timeout')
  end

  it 'rolls back on partial failure' do
    allow(InventoryService).to receive(:reserve)
      .and_raise(InventoryService::OutOfStock)

    expect { described_class.new.call(valid_params) }
      .not_to change(Order, :count)
  end
end

Background Job Error Tests

RSpec.describe ProcessOrderJob, type: :job do
  it 'retries on timeout errors' do
    allow(OrderProcessor).to receive(:process!)
      .and_raise(Net::OpenTimeout)

    expect {
      described_class.perform_now(order.id)
    }.to have_enqueued_job(described_class)
  end

  it 'discards when order not found' do
    expect {
      described_class.perform_now('deleted-id')
    }.not_to raise_error
  end
end

Each production error caught by Bugsly should inspire a new test. This feedback loop ensures your test suite grows with real-world failure patterns.

Try Bugsly Free

AI-powered error tracking that explains your bugs. Set up in 2 minutes, free forever for small projects.

Get Started Free