Published on

How to Write a Spec with RSpec

Authors

Testing your code effectively is crucial to maintaining the stability and reliability of your software. One of the most popular tools for writing tests in Ruby is RSpec. This post provides a step-by-step guide on how to write a well-structured spec, complete with annotated code snippets and detailed explanations.



RSpec 101: A Practical Guide

Running a Spec

Before diving into writing specs, here’s a simple command to run your tests:

shell
rspec spec/path/to/file_spec.rb

A single line of code like this can test entire files or specific specs. Remember, concise and clear commands are key to efficient testing.

Anatomy of a Spec

Let's break down an annotated RSpec spec and understand the syntax and structure. Note that some parts have been omitted or simplified for clarity.

ruby
# spec/channels/application_cable/connection_spec.rb

# Always require 'rails_helper' at the top
require 'rails_helper.rb'

RSpec.describe ApplicationCable::Connection, type: :channel do

  # Top-level `before` block for general setup
  before do
    allow(described_class).to receive(:env).and_return(env)
    ...
  end

  context 'with a verified user' do
    # Nested `before` blocks to handle specific scenarios
    before { exec_command }

    it "successfully connects" do
      ...
    end
  end

  context 'without a verified user' do
    # Using lazy loading for variables
    let(:user) { nil }
    let(:headers) { {} }

    it "rejects connection" do
      expect(exec_command_proc).to have_rejected_connection
    end
  end

  # Always use `private` to separate helper methods from the main spec
  private

  # Define methods before variable assignments for clarity
  def exec_command
    connect "/cable", headers: headers
  end

  def exec_command_proc
    method(:exec_command).to_proc
  end

  # Lazy loaded variable definitions
  let(:user) { instance_double(User, id: id) }
  let(:env) { instance_double('env') }
  let(:headers) { { "X-USER-ID" => id } }
end

Key Concepts

describe and context

Start your spec with an RSpec.describe block, which describes the object or method you're testing. Use describe for methods and context to specify different scenarios. While they function the same way, using both enhances readability.

Convention Tip: Use describe '#instance_method' for instance methods and describe '.class_method' for class methods.

described_class

Refer to the class under test using described_class. This improves readability and makes your tests more maintainable.

In the example above:

ruby
RSpec.describe ApplicationCable::Connection, type: :channel do
  before do
    allow(described_class).to receive(:env).and_return(env)
    ...

Would be equivalent to:

ruby
RSpec.describe ApplicationCable::Connection, type: :channel do
  before do
  	allow(ApplicationCable::Connection).to receive(:env).and_return(env)
    ...

When not testing a class, use a string like for example:

ruby
RSpec.describe 'Some Module - Update API' do
  # Use described_class instead of repeating the module or class name
end

type

Specify the type of spec with the type: argument. This activates special RSpec DSLs, like :channel for Action Cable channels. You can also add metadata like skip: true or custom tags for more control.

Common Blocks: subject and before

subject

subject is similar to a let block but used for the primary object under test. It simplifies expectations with a more natural syntax.

ruby
it 'has a result of zero' do
  expect(something).to be_zero
end

becomes:

ruby
subject { something }

it 'is expected to be zero' do
  is_expected.to be_zero
end

before

Use before for setup and after for teardown. Both can take arguments, like before(:each) or after(:all). :each is the default, running before each it block.

Lazy Loading with let and let!

let is used for defining variables lazily. They’re only instantiated when referenced, keeping your specs clean. Use let! if you need eager loading.

ruby
let(:variable) { 'something' }

This keeps your spec clean and optimizes performance. Avoid direct variable assignment in specs.

Making Assertions: it and expect

The it block is where you make assertions using expect. Write readable, human-like sentences to make your specs understandable to others.

ruby
it 'returns a valid response' do
  expect(response).to be_successful
end

Writing Helper Methods

Place helper methods at the bottom of your spec, under private, along with let assignments to keep everything organized.

Mocks, Stubs, and Spies

Use allow to create stubs:

ruby
allow(described_class).to receive(:env).and_return(env)

For stubbing objects, use instance_double:

ruby
let(:user) { instance_double(User, id: id) }

Instance doubles mimic real objects, responding to methods you specify. For more control, you can use spy to track interactions.

Wrapper Methods: exec_command

The exec_command and exec_command_proc methods standardize how actions are executed in your tests. These are personal preferences and not part of the RSpec framework. exec_command_proc allows you to test if a command raises an error:

ruby
it 'raises an error' do
  expect(exec_command_proc).to raise_error
end

Conclusion

This guide provides a concise yet comprehensive look at writing effective RSpec tests. By structuring your specs well, you not only ensure your code behaves as expected but also make your tests readable and maintainable for your team.