Blog

RSpec Metadata - What They Are and How to Use Them

Placeholder Avatar
Sameera Gayan
November 22, 2019

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

ruby # 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]:

ruby # 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:

ruby # 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:

ruby # 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:

ruby # 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..)

ruby # 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:

ruby # 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:

ruby 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:

ruby 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:

ruby 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:

ruby # 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…