Rails Testing with RSpec — Ruby Deep Dive [23]

Bhavyansh @ DiversePixel
6 min readSep 8, 2024

--

Testing is crucial in Ruby on Rails development. This article explores RSpec and associated tools that enhance the testing experience in Rails applications.

Key Tools

1. RSpec: A behavior-driven development framework for Ruby.
2. Factory Bot Rails: A library for setting up Ruby objects as test data.
3. Shoulda Matchers: Provides additional RSpec matchers for common Rails functionality.
4. Faker: Generates realistic fake data for tests.
5. RuboCop: A Ruby static code analyzer and formatter.
6. SimpleCov: A code coverage analysis tool for Ruby.
7. Rails Controller Testing: Provides helpers for testing Rails controllers.

Updated RSpec Configuration:

We make these following changes to spec/rails_helper.rb file.

SimpleCov Configuration

# At the top of file
require 'simplecov'
SimpleCov.start 'rails'
  1. This configuration sets up SimpleCov, a code coverage analysis tool for Ruby.
  2. SimpleCov.start 'rails' initializes SimpleCov with Rails-specific settings.
  3. It will track which parts of your code are covered by your test suite.
  4. After running your tests, SimpleCov generates a coverage report.

Factory Bot Syntax Methods

config.include FactoryBot::Syntax::Methods
  1. This line includes Factory Bot’s syntax methods in all of your tests.
  2. It allows you to use factory methods (like create, build, attributes_for) without prefixing them with FactoryBot.
  3. For example, you can write create(:user) instead of FactoryBot.create(:user).

Devise Test Helpers

config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view
config.include Devise::Test::IntegrationHelpers, type: :feature
  1. These lines include Devise’s test helper methods in your specs.
  2. ControllerHelpers are included in controller and view specs, providing methods like sign_in and sign_out.
  3. IntegrationHelpers are included in feature specs, allowing you to use sign_in and sign_out in integration tests.
  4. These helpers make it easier to test authentication-related functionality.

Shoulda Matchers Configuration

Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
  1. This block configures Shoulda Matchers, a library that provides additional RSpec matchers.
  2. It integrates Shoulda Matchers with RSpec and Rails.
  3. This configuration allows you to use matchers like validate_presence_of, have_many, belong_to, etc., in your specs.
  4. These matchers are particularly useful in model specs for testing validations and associations.

RSpec: Core Concepts

RSpec uses a domain-specific language (DSL) to describe the expected behavior of your code. Key methods include:

- `describe`: Groups related tests
- `context`: Provides specific scenarios within a describe block
- `it`: Defines individual test cases
- `expect`: Asserts expected outcomes

For a comprehensive list of available methods, explore the directories in the rspec-rails repository: https://github.com/rspec/rspec-rails/tree/main/lib

Overview of a Controller Spec

Key Aspects and Frequently Used Methods

  1. RSpec.describe - Defines a group of related specs for the controller.
  2. let - Creates helper methods for test data. Example: let(:owner) { create(:user, :owner) }
  3. before - Sets up preconditions for a group of tests.
  4. it - Defines individual test cases.
  5. expect - Makes assertions about the behavior of the code being tested.
  6. create and create_list - Factory Bot methods for creating test data.
  7. sign_in - A Devise helper method for authenticating users in tests.
  8. get, post, etc. - HTTP verb methods for simulating requests.
  9. assigns - Checks instance variables set by the controller action.
  10. response - Allows assertions about the HTTP response.
  11. render_template - Checks if a specific template was rendered.
  12. redirect_to - Checks if a redirect occurred.
  13. allow and receive - RSpec mocking methods for stubbing behavior.

Comparison with Model and Request Specs

Model Specs

  • Focus on testing business logic and data integrity.
  • Often use methods like valid?, errors, and test ActiveRecord associations and validations.
  • Don’t involve HTTP requests or controller logic.
  • Example methods: be_valid, have_many, validate_presence_of

Request Specs

  • Test the full stack, including routing, controller actions, and sometimes even view rendering.
  • More closely simulate how the application is used by sending actual HTTP requests.
  • Often use methods like get, post, patch, delete to simulate HTTP verbs.
  • Assert against the response object for status codes, body content, etc.
  • Example methods: have_http_status, match_json_schema

Frequently Used RSpec Methods

  1. describe and context - Group related tests.
  2. before and after - Set up and tear down test environments.
  3. let and let! - Define memoized helper methods.
  4. subject - Define the primary object under test.
  5. described_class - Reference the class being tested.
  6. expect().to and expect().not_to - Make positive and negative assertions.
  7. be_a, be_an_instance_of, be_truthy, be_falsey - Matchers for type and truthiness checks.
  8. change - Test for changes in values.
  9. raise_error - Test for raised exceptions.

Factories

Let’s break down the following factory definition:

FactoryBot.define do
factory :order do
association :visitor, factory: :user
restaurant
status { :pending }
total_price { Faker::Commerce.price(range: 10.0..100.0) }

trait :with_items do
transient do
items_count { 3 }
end

after(:create) do |order, evaluator|
create_list(:order_item, evaluator.items_count, order: order)
order.update(total_price: order.order_items.sum { |item| item.quantity * item.menu_item.price })
end
end
end
end

Line-by-Line Explanation

FactoryBot.define do

  • This line begins the factory definition block. All factories are defined within this block.

factory :order do

  • Defines a factory for the Order model. The symbol :order is used to name the factory.

association :visitor, factory: :user

  • Creates an association for the visitor attribute.
  • It uses the :user factory to create this association.
  • This is equivalent to setting order.visitor = FactoryBot.create(:user).

restaurant

  • Creates an association for the restaurant attribute.
  • It implicitly uses a factory named :restaurant.
  • This is a shorthand for association :restaurant.

status { :pending }

  • Sets the status attribute of the order to :pending.
  • The curly braces {} allow for dynamic values or lazy evaluation.

total_price { Faker::Commerce.price(range: 10.0..100.0) }

  • Uses the Faker gem to generate a random price between 10.0 and 100.0 for the total_price attribute.

trait :with_items do

  • Defines a trait named :with_items. Traits allow you to group attributes together and apply them selectively.

transient do

  • Begins a block for transient attributes. These are not directly assigned to the model but can be used within the factory.

items_count { 3 }

  • Defines a transient attribute items_count with a default value of 3.

after(:create) do |order, evaluator|

  • Defines a callback that runs after the order is created.
  • It takes two arguments: the created order and an evaluator object.

create_list(:order_item, evaluator.items_count, order: order)

  • Creates a list of order items.
  • Uses the :order_item factory to create items_count number of items.
  • Associates each item with the current order.

order.update(total_price: ...)

  • Updates the order’s total price after creating the items.
  • Calculates the sum of (quantity * price) for all order items.

Usage

To use this factory:

  • Basic order: FactoryBot.create(:order)
  • Order with items: FactoryBot.create(:order, :with_items)
  • Order with custom item count: FactoryBot.create(:order, :with_items, items_count: 5)

Factory Bot Concepts

Traits

Traits are a powerful feature in Factory Bot that allow you to group attributes together and apply them selectively to your factories. They provide a way to define different variations of a factory without duplicating code.

Key Points about Traits:

  1. Definition: Traits are defined within a factory using the trait method.
  2. Reusability: They can be reused across multiple factories.
  3. Modularity: Traits allow you to modularize your factory definitions, making them more maintainable.
  4. Combination: Multiple traits can be applied to a single factory instance.
  5. Overriding: Traits can override attributes defined in the main factory.

Example:

factory :user do
name { "John Doe" }

trait :admin do
admin { true }
role { "administrator" }
end

trait :with_posts do
after(:create) do |user|
create_list(:post, 3, user: user)
end
end
end
# Usage:
FactoryBot.create(:user) # Regular user
FactoryBot.create(:user, :admin) # Admin user
FactoryBot.create(:user, :with_posts) # User with posts
FactoryBot.create(:user, :admin, :with_posts) # Admin user with posts

Other Factory Bot-Specific Concepts

1. Associations

Associations in Factory Bot allow you to define relationships between models in your factories.

factory :post do
association :author, factory: :user
end

2. Sequences

Sequences generate unique values, useful for attributes that must be unique.

sequence :email do |n|
"user#{n}@example.com"
end
factory :user do
email
end

3. Transient Attributes

Transient attributes are used within the factory but not assigned directly to the model.

factory :user do
transient do
upcased { false }
end

name { "John Doe" }

after(:create) do |user, evaluator|
user.name.upcase! if evaluator.upcased
end
end
# Usage:
FactoryBot.create(:user, upcased: true)

4. Callbacks

Callbacks allow you to run code at different points in the object creation process.

factory :user do
after(:create) do |user|
user.generate_auth_token
end
end

5. Inheritance

Factories can inherit from other factories, allowing for specialization.

factory :post do
title { "A Regular Post" }
end
factory :featured_post, parent: :post do
featured { true }
end

These concepts make Factory Bot a powerful tool for creating flexible, reusable test data in your Rails applications. They allow you to create complex scenarios easily, keeping your tests clean and maintainable.

Best Practices for Rails testing

1. Use `describe` for classes and `context` for specific states or scenarios.
2. Write descriptive test names using `it`. (Refer to this guide for more details).
3. Use factories to create test data instead of building objects manually.
4. Utilize Shoulda Matchers for common validations and associations.
5. Employ Faker for generating realistic, randomized data.
6. Run RuboCop regularly to maintain code quality and consistency.
7. Monitor test coverage with SimpleCov and aim for high coverage.

Conclusion

Mastering RSpec and its ecosystem of testing tools can significantly improve the quality and maintainability of your Rails applications. By following best practices and leveraging these powerful tools, you can create a robust test suite that gives you confidence in your code.

--

--

Bhavyansh @ DiversePixel

Hey I write about Tech. Join me as I share my tech learnings and insights. 🚀