RSpec Metadata: Powering Your Tests

Why Metadata Matters

Imagine you’re building a Learning Management System (LMS). You have tests for user registration, course creation, and grading. Some tests might be quick, while others might involve complex database interactions. You want to run only the tests you need, when you need them. This is where metadata comes in.

Metadata is like adding labels or tags to your tests. These labels provide extra information about the test, allowing you to control how and when they run. It’s a powerful way to organize and manage your tests, making them faster, more reliable, and easier to use.

Key Benefits of Using Metadata:

  • Run Only What You Need: Focus on specific tests, saving time and resources.
  • Isolate Failures: Quickly identify and fix problems by running only the failing tests.
  • Avoid Unnecessary Setup: Skip expensive setup code when it’s not required.
  • Customize Test Behavior: Change how tests run based on their metadata.

Understanding Metadata

Think of metadata as a hidden dictionary (or hash) attached to each of your tests. This dictionary stores information about the test, such as:

  • Example Configuration: Whether the test is skipped, pending, or focused.
  • Source Code Location: The file and line number where the test is defined.
  • Status of Previous Runs: Whether the test passed, failed, or is pending.
  • Specific Requirements: Information like which database or browser to use.

RSpec automatically adds some default metadata, and you can add your own custom metadata.

RSpec’s Default Metadata

Let’s see what RSpec automatically adds to the metadata.

require 'rspec'
require 'pp' # Pretty print for easier reading

RSpec.describe 'Course' do
  it 'has a name' do |example|
    pp example.metadata
  end
end

Explanation:

  1. require 'rspec' and require 'pp': We load the RSpec testing library and the pretty print library.
  2. RSpec.describe 'Course': This starts a test group for the Course class.
  3. it 'has a name' do |example|: This defines a single test case. The |example| part is important. It allows us to access the current test’s metadata.
  4. pp example.metadata: This prints the metadata hash to the console.

If you run this code, you’ll see a lot of information. Here are some key entries:

  • :description: The string you passed to it (e.g., “has a name”).
  • :full_description: The combined description from describe and it (e.g., “Course has a name”).
  • :described_class: The class passed to the outermost describe block (e.g., Course).
  • :file_path: The path to the file containing the test.
  • :example_group: Metadata from the enclosing describe block.
  • :last_run_status: The status of the test from the last run (e.g., “unknown”).

Adding Custom Metadata

You can add your own metadata to tests. This is where the real power of metadata comes in.

RSpec.describe 'Student' do
  it 'can enroll in a course', fast: true, database: :test do |example|
    pp example.metadata
  end

  it 'can view grades', :slow do |example|
    pp example.metadata
  end
end

Explanation:

  1. fast: true, database: :test: We’ve added two custom metadata entries to the first test.
  2. :slow: This is a shortcut for :slow => true.

Now, if you run this code, you’ll see the custom metadata in the output.

Metadata Inheritance

Metadata can be inherited. If you add metadata to a describe block, all the tests inside it will inherit that metadata.

RSpec.describe 'Assignment', database: :test do
  it 'can be created' do |example|
    pp example.metadata
  end

  context 'when graded' do
    it 'has a score' do |example|
      pp example.metadata
    end
  end
end

Explanation:

  1. database: :test is added to the describe block.
  2. Both tests inside the describe block will inherit this metadata.

Derived Metadata

You can automatically add metadata to tests based on certain conditions using config.define_derived_metadata.

RSpec.configure do |config|
  config.define_derived_metadata(file_path: /spec\/models/) do |meta|
    meta[:model_test] = true
  end
end

RSpec.describe 'Course', file_path: 'spec/models/course_spec.rb' do
  it 'has a title' do |example|
    pp example.metadata
  end
end

Explanation:

  1. config.define_derived_metadata(file_path: /spec\/models/): This tells RSpec to add metadata to tests in files matching the regular expression /spec\/models/.
  2. meta[:model_test] = true: This adds the :model_test metadata to the matching tests.

Default Metadata

You can set default metadata for all tests, but allow individual tests to override it.

RSpec.configure do |config|
  config.define_derived_metadata do |meta|
    meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
  end
end

RSpec.describe 'Quiz' do
  it 'has multiple questions' do |example|
    pp example.metadata
  end

  it 'can be graded', aggregate_failures: false do |example|
    pp example.metadata
  end
end

Explanation:

  1. meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures): This sets :aggregate_failures to true by default, but only if it’s not already set.
  2. The second test explicitly sets :aggregate_failures to false, overriding the default.

Reading Metadata

You’ve already seen how to access metadata using the example argument in it blocks. You can also access it in hooks and let declarations.

RSpec.configure do |config|
  config.around(:example, :database) do |example|
    puts "Starting test: #{example.metadata[:full_description]}"
    example.run
    puts "Ending test: #{example.metadata[:full_description]}"
  end
end

RSpec.describe 'Enrollment' do
  let(:enrollment_service) do |example|
    EnrollmentService.new(database: example.metadata[:database])
  end

  it 'creates a new enrollment', database: :test do
    # ...
  end
end

Explanation:

  1. config.around(:example, :database): This hook runs around tests with the :database metadata.
  2. let(:enrollment_service) do |example|: This let declaration accesses the metadata.

Selecting Which Specs to Run

You can use metadata to filter which tests to run.

Excluding Examples

RSpec.configure do |config|
  config.filter_run_excluding :slow
end

This will exclude all tests with the :slow metadata.

Including Examples

RSpec.configure do |config|
  config.filter_run_including :fast
end

This will only run tests with the :fast metadata.

Command Line

You can also filter tests from the command line:

rspec --tag fast
rspec --tag '~slow' # Exclude slow tests
rspec --tag database:test # Run tests with database: :test

Sharing Code Conditionally

You can use metadata to share code conditionally using hooks, modules, and shared contexts.

RSpec.configure do |config|
  config.before(:example, :database) do
    DB.connect
  end

  config.after(:example, :database) do
    DB.disconnect
  end
end

This will only run the database connection code for tests with the :database metadata.

Changing How Your Specs Run

Metadata can change how RSpec behaves.

  • :aggregate_failures: Runs all expectations in a test, even if one fails.
  • :pending: Marks a test as expected to fail.
  • :skip: Skips a test entirely.
  • :order: Sets the order in which tests run.
RSpec.describe 'Grade' do
  it 'calculates the average', aggregate_failures: true do
    expect(1 + 1).to eq(3)
    expect(2 + 2).to eq(5)
  end

  it 'is pending', pending: 'Waiting for API' do
    # ...
  end

  it 'is skipped', skip: 'Not implemented yet' do
    # ...
  end
end


Posted

in

by