Skip to content
PLAY VIDEO PLAY VIDEO PLAY VIDEO
By Sameera Gayan

RSpec metadata - what they are and how to use them

If you are using RSpec and are thinking that you've never used metadata, chances are that you have already been using them without even knowing.

In this blog post, let's discuss what RSpec metadata is and how can we can have our own to make our specs more readable and clean.

What is metadata?

Metadata is basically a set of additional instructions (typically a Ruby hash) that you can send to a spec that it can then use. At a high level, this extra information can be used to:

  • Load additional files
  • Set values
  • Do actions (e.g before actions), etc.

But you said I might be already using it?

Yes, I did. So, let's dive into some examples. When writing specs, you may be familiar with the following syntax:

Example 1. using metadata in RSpec itself

# https://relishapp.com/rspec/rspec-rails/docs/model-specs
require "rails_helper"

RSpec.describe Post, type: :model do
  context "with 2 or more comments" do
    it "orders them in reverse chronologically" do
      # your test code here
    end
  end
end

The part type: :model is the metadata. It sends additional instructions to the spec. In this case, it tells the spec "this is a model test". In doing so, the spec will then know what libraries need to be loaded to work with model tests.

In RSpec, you have two ways of letting the framework know what kind of spec you are running:

  1. By structuring your spec in the correct folders. For example, model specs should go in spec/models/
  2. Letting RSpec know explicitly by sending the type as a metadata argument, as in the above example.

Below is an oversimplified version of how this works. Note that this is from RSpec's master branch, so it could change over time.

  • Sets the type (either from the folder structure, via DIRECTORY_MAPPINGS hash) or a metadata[:type]:
    # https://github.com/rspec/rspec-rails/blob/master/lib/rspec/rails/configuration.rb#L111
    define_derived_metadata(:file_path => escaped_path) do |metadata|
      metadata[:type] ||= type
    end

  • Then based on the type, loads necessary helpers:
# https://github.com/rspec/rspec-rails/blob/master/lib/rspec/rails/configuration.rb#L7
# Automatically tag specs in conventional directories with matching `type`
# metadata so that they have relevant helpers available to them

Example 2. Capybara

The Capybara gem is another good (and easy to understand) example of how metadata can be used to give extra information.

From the Capybara README:

# https://github.com/teamcapybara/capybara#using-capybara-with-rspec
Use js: true to switch to the Capybara.javascript_driver (:selenium by default), or provide a :driver option to switch to one specific driver.

By way of an example:

# https://github.com/teamcapybara/capybara#using-capybara-with-rspec
describe 'some stuff which requires js', js: true do
  it 'will use the default js driver'
  it 'will switch to one specific driver', driver: :apparition
end

And this has been implemented as:

# https://github.com/teamcapybara/capybara/blob/master/lib/capybara/rspec.rb#:L23
config.before do |example|
  if self.class.include?(Capybara::DSL)
    Capybara.current_driver = Capybara.javascript_driver if example.metadata[:js]
    Capybara.current_driver = example.metadata[:driver] if example.metadata[:driver]
  end
end

As you can see on the above code, if the user passes metadata as js, it will load the Capybara.javascript_driver as the current driver.

Cool story... but you said I can implement this too?

Yes, I did. Let's go through how can we use our own custom metadata. For this, we'll consider a very basic example.

Example app

An app that has two types of user roles. user (normal user) and admin. When logged in:

  • users can see Profile link.
  • admins can see, Profile link and also Settings link.

We'll first write a very basic request spec for this. (At this point I'm assuming the project is set up with RSpec, FactoryBot, Capybara, etc..)

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    ...
  end

  factory :admin, class: User do
    ...
  end
end

And our request spec would look like this:

# spec/features/login_feature_spec.rb
require 'rails_helper'

RSpec.feature 'Login', type: :feature do
  before do
    sign_in user
    visit root_path
  end

  context 'user login' do
    let!(:user) { create :user }

    it 'shows the correct navigation links' do
      expect(page).to have_link('Profile')
      expect(page).not_to have_link('Settings')
    end
  end

  context 'admin login' do
    let!(:user) { create :admin }

    it 'shows the correct navigation links' do
      expect(page).to have_link('Profile')
      expect(page).to have_link('Settings')
    end
  end
end

# Finished in 0.17118 seconds (files took 0.69104 seconds to load)
2 examples, 0 failures

Now, let's try to implement this using metadata. As you can see in this example, the common part for both of these specs is the user login. Basically, we pass a user object and it logs in that user. It would be nice to read the specs as:

RSpec.feature 'Login', type: :feature do
  context 'user login', login_as: :user do
    ...
  end

  context 'admin login', login_as: :admin do
    ...
  end
end

Now, let's see how we can achieve this. As you can see, we pass the data as login_as: :user where login_as is the key and :user is the value. In our case, the value will be directly matched to a FactoryBot factory.

We will first create a separate support file to handle the login. NOTE: you can add the following code to either rails_helper or spec_helper, but I would highly recommend you have it in a separate file for clarity and easy maintenance in the future.

Create a support file spec/support/login.rb, and add the following code:

RSpec.configure do |config|
  config.before do |example|
    meta =  example.metadata
    if meta[:type] == :feature && meta[:login_as]
      sign_in create(meta[:login_as])
      visit root_path
    end
  end
end

I'll break the code down for you

  • The RSpec.configure block runs for each example, so we need to enclose our code inside that configure block.
  • On the next line config.before we tell RSpec to run this code before each example. The options are:
    • config.before (the one we have)
    • config.around
    • config.after
  • The next important line is if meta[:type] == :feature && meta[:login_as]. Here we check if the spec is a feature spec and also if the login_as metadata is given. We can just go by meta[:login_as], but I've added meta[:type] just as an extra check.
  • The rest is a pretty standard code, where we create and sign in the user factory from the metadata value and redirect the user to the root_path.

The important takeaway of this code is:

RSpec.configure do |config|
  config.<your execution type> do |example|
    # Your code to handle the example.metadata.
  end
end

With all that done, the final code will be:

# spec/support/login.rb
RSpec.configure do |config|
  config.before do |example|
    meta =  example.metadata
    if meta[:type] == :feature && meta[:login_as]
      sign_in create(meta[:login_as])
      visit root_path
    end
  end
end

# spec/features/login_feature_spec.rb
require 'rails_helper'
RSpec.feature 'Login', type: :feature do
  context 'user login', login_as: :user do
    it 'shows the correct navigation links' do
      expect(page).to have_link('Profile')
      expect(page).not_to have_link('Settings')
    end
  end

  context 'admin login', login_as: :admin do
    it 'shows the correct navigation links' do
      expect(page).to have_link('Profile')
      expect(page).to have_link('Settings')
    end
  end
end

Finished in 0.21993 seconds (files took 4.63 seconds to load)
2 examples, 0 failures

Awesome. I'm going to be using metadata for everything from here on out

Hmm, maybe not! You see, metadata is a very powerful concept in RSpec, however, like everything in life, overusing it will make your specs unreadable and hard to maintain.

Ideally, a spec should have everything in one file for easy reading/reference. If not, It's important to make sure you can read and understand the spec without switching between files.

I personally think, context 'admin login', login_as: :admin reads better and makes it easier to understand what is expected in the spec. However, I wouldn't go and add multiple metadata as it will make the spec too complex and hard to understand.

Any final words?

Yes. Now you know about RSpec metadata (if you didn't already). Remember this is another option you can use to write more readable specs. But the most important thing is to understand when to use it.

I personally believe that using this the right way will make your specs much cleaner and easy to understand.

Happy coding...

Latest Articles by Our Team

Our expert team of designers and developers love what the do and enjoy sharing their knowledge with the world.

We Hire Only the Best

reinteractive is Australia’s largest dedicated Ruby on Rails development company. We don’t cut corners and we know what we are doing.

We are an organisation made up of amazing individuals and we take pride in our team. We are 100% remote work enabling us to choose the best talent no matter which part of the country they live in. reinteractive is dedicated to making it a great place for any developer to work.

Free Community Workshops

We created the Ruby on Rails InstallFest and Ruby on Rails Development Hub to help introduce new people to software development and to help existing developers hone their skills. These workshops provide invaluable mentorship to train developers, addressing key skills shortages in the industry. Software development is a great career choice for all ages and these events help you get started and skilled up.

  • Webinars

    Webinars

    Webinars are our online portal for tips, tricks and lessons learned in everything we do. Make the most of this free resource to help you become a better developer.

    Learn more about webinars

  • Installfest

    Installfest

    The Ruby on Rails Installfest includes a full setup of your development environment and step-by-step instructions on how to build your first app hosted on Heroku. Over 1,800 attendees to date and counting.

    Learn more about Installfest

  • Development Hub

    Development Hub

    The Ruby on Rails Development Hub is a monthly event where you will get the chance to spend time with our team and others in the community to improve and hone your Ruby on Rails skills.

    Learn more about Development Hub

Get the “reinteractive Review” Monthly Email