Techdots

July 22, 2025

Testing SQL queries in a Ruby service

Testing SQL queries in a Ruby service

Have you ever wondered why your Ruby application suddenly becomes slow or returns wrong data? The answer often lies in untested SQL queries. Many developers focus on testing Ruby code but forget about SQL Query Testing - a critical part that can make or break your application.

When SQL queries aren't properly tested, you risk facing data corruption, performance issues, and unexpected failures in production. This is especially important for Ruby Services where database interactions are at the core of your business logic.

In this comprehensive guide, we'll walk you through everything you need to know about testing SQL queries in Ruby services, from basic RSpec setup to advanced performance testing techniques.

What Is SQL Query Testing?

SQL Query Testing is the practice of writing automated tests that verify your database queries work correctly. Instead of just testing Ruby methods, you also test the actual SQL that runs against your database.

In Ruby Services, this means testing both ActiveRecord scopes and raw SQL queries to ensure they:

  • Return the correct data
  • Perform efficiently
  • Handle edge cases properly
  • Don't cause data corruption

The Foundation: Automated Testing for SQL

Automated Testing for SQL queries follows the same principles as regular code testing. You set up test data, run your query, and check if the results match your expectations.

The key difference is that Database Testing requires a real database connection. While you might mock other dependencies, SQL queries need actual database interaction to verify they work correctly.

Why Real Databases Beat Mock Databases?

Many developers try to use Mock Databases for testing, but this approach has serious limitations:

  • Mocks don't catch SQL syntax errors
  • They can't verify query performance
  • Database-specific features might not work as expected
  • You miss issues with joins, subqueries, and complex conditions

Quality Assurance for SQL requires testing against real database instances, even if they're lightweight test databases.

Setting Up RSpec for SQL Testing

RSpec for SQL testing follows standard RSpec patterns. Here's how to structure your tests effectively:

Testing ActiveRecord Scopes


RSpec.describe User, type: :model do
  describe ".active" do
    it "returns only users with active status" do
      active_user = FactoryBot.create(:user, active: true)
      _inactive_user = FactoryBot.create(:user, active: false)
      result_ids = User.active.pluck(:id)
      expect(result_ids).to contain_exactly(active_user.id)
    end
  end
end
  

This test creates known data and verifies the scope returns exactly what you expect.

Testing Raw SQL Queries

For services that use raw SQL, you can test the actual query execution:


class ReportService
  def self.total_sales
    result = ActiveRecord::Base.connection.execute("SELECT SUM(amount) FROM orders")
    result.getvalue(0, 0).to_i
  end
end
  

Test it like this:


RSpec.describe ReportService do
  describe ".total_sales" do
    it "sums the order amounts" do
      Order.create!(amount: 100)
      Order.create!(amount: 50)
      expect(ReportService.total_sales).to eq(150)
    end
  end
end
  

Managing Test Data with Transaction Testing

Transaction Testing is crucial for keeping your tests isolated and fast. Each test should start with a clean database state.

Rails Setup with Transactions

In Rails, you can use transactional fixtures:


RSpec.configure do |config|
  config.use_transactional_fixtures = true
end
  

This wraps each test in a database transaction that rolls back after the test completes.

Using Database Cleaner

For more control, use Database Cleaner:


RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end
  

Testing Query Performance

Database Testing isn't just about correctness - performance matters too. You can test query speed using gems like rspec-benchmark:


RSpec.describe ReportService do
  it "executes total_sales query under 50ms" do
    FactoryBot.create_list(:order, 1000, amount: 1)
    expect { ReportService.total_sales }.to perform_under(0.05).sec
  end
end
  

Using EXPLAIN for Query Analysis

You can analyze query performance using EXPLAIN:


plan = ActiveRecord::Base.connection.execute(
  "EXPLAIN ANALYZE #{User.where(active: true).to_sql}"
).to_a
  

This helps identify slow queries and missing indexes before they become production problems.

Practical Testing Examples

Example 1: Testing Complex Scopes


RSpec.describe Book, type: :model do
  describe ".published" do
    it "returns books with a published_at date" do
      published_book = FactoryBot.create(:book, published_at: 1.day.ago)
      _draft_book    = FactoryBot.create(:book, published_at: nil)
      results = Book.published
      expect(results).to include(published_book)
      expect(results).not_to include(_draft_book)
    end
  end
end
  

Example 2: Testing Service Objects with Raw SQL


class UserReport
  def self.active_customers_last_month
    sql = <<-SQL
      SELECT users.id, users.name, COUNT(orders.id) AS orders_count
      FROM users
      JOIN orders ON orders.user_id = users.id
      WHERE orders.created_at >= DATE('now', '-1 month')
      GROUP BY users.id, users.name
    SQL
    ActiveRecord::Base.connection.exec_query(sql)
  end
end
  

RSpec.describe UserReport do
  describe ".active_customers_last_month" do
    it "returns users who placed orders in the last month with their order count" do
      # Setup test data
      user1 = FactoryBot.create(:user, name: "Alice")
      user2 = FactoryBot.create(:user, name: "Bob")
      # Orders within the last month (should be included)
      FactoryBot.create_list(:order, 2, user: user1, created_at: 10.days.ago)
      # Orders older than a month (should be excluded)
      FactoryBot.create(:order, user: user2, created_at: 40.days.ago)
      # Exercise
      result = UserReport.active_customers_last_month
      # Verify
      names = result.map { |row| row["name"] }
      expect(names).to contain_exactly("Alice")
      expect(result.first["orders_count"]).to eq(2)
    end
  end
end
  

Example 3: When to Use Mock Databases

Sometimes Mock Databases are appropriate - mainly for error handling:


it "returns 0 if the database query times out" do
  # Simulate a timeout on the raw SQL call
  allow(ActiveRecord::Base.connection).to receive(:execute)
    .with(/SELECT SUM\(amount\)/)
    .and_raise(ActiveRecord::QueryCanceled)
  # Expect graceful fallback to 0
  expect(ReportService.total_sales).to eq(0)
end
  

Use mocks only for testing error scenarios, not for primary query logic.

Best Practices for SQL Testing

Here are the best practices for SQL testing: 

1. Test Edge Cases

Always test scenarios like:

  • No matching records
  • NULL values
  • Boundary conditions
  • Empty result sets

2. Keep Tests Fast

  • Use minimal test data
  • Leverage transactions for cleanup
  • Consider in-memory SQLite for simple tests

3. Include Tests in CI/CD

Make sure your continuous integration runs Database Testing with proper database setup.

4. Monitor Performance

Use EXPLAIN to understand query plans and catch performance regressions early.

5. Test Real Scenarios

Create test data that represents realistic production conditions.

Setting Up Test Environments

For Rails Applications

Rails handles most test database setup automatically through config/database.yml. Use FactoryBot for test data creation and rely on transactional fixtures for isolation.

For Non-Rails Ruby Services

Set up ActiveRecord manually:


# spec/spec_helper.rb or test/test_helper.rb
require 'active_record'
require 'sqlite3'
# Establish in-memory DB connection
ActiveRecord::Base.establish_connection(
  adapter: 'sqlite3',
  database: ':memory:'
)
# Load your schema (you can also use migrations or raw SQL here)
ActiveRecord::Schema.define do
  create_table :users, force: true do |t|
    t.string :name
    t.boolean :active
  end
  create_table :orders, force: true do |t|
    t.integer :user_id
    t.integer :amount
    t.datetime :created_at
  end
end
  

Load your schema and create necessary tables before running tests.

Debugging SQL Issues

When tests fail, check:

  1. The actual SQL being generated (use .to_sql on ActiveRecord relations)
  2. Database logs for executed queries
  3. EXPLAIN output for performance issues
  4. Test data setup - make sure you have the right records

Conclusion

SQL Query Testing is essential for reliable Ruby Services. By combining RSpec for SQL with proper Transaction Testing and Quality Assurance for SQL practices, you'll catch bugs early and maintain high-performance applications. Don't let untested queries become production nightmares.

Ready to improve your Ruby application's reliability? Contact TechDots today for expert guidance on implementing comprehensive database testing strategies that will save you time and prevent costly production issues.

Ready to Launch Your AI MVP with Techdots?

Techdots has helped 15+ founders transform their visions into market-ready AI products. Each started exactly where you are now - with an idea and the courage to act on it.

Techdots: Where Founder Vision Meets AI Reality

Book Meeting