RSpec’s metadata solves a very specific problem: where do I keep information about the context my specs are running in? By context, we mean things like:
Without some way of attaching data to examples, you (and the RSpec maintainers!) would be stuck juggling global variables and writing a bunch of bookkeeping code.
RSpec’s solution to this problem couldn’t be simpler: a plain Ruby hash. Every example and example group gets its own such hash, known as the metadata hash. RSpec populates this hash with any metadata you’ve explicitly tagged the example with, plus some useful entries of its own.
A good example is worth a thousand words, so let’s jump right into one. In a fresh directory, create a file called metadata_spec.rb with the following contents:
| require 'pp' |
| |
| RSpec.describe Hash do |
| it 'is used by RSpec for metadata' do |example| |
| pp example.metadata |
| end |
| end |
This snippet shows something we haven’t talked about before: getting access to your example’s properties at runtime. You can do so by having your it block take an argument. RSpec will pass an object representing the currently running example. We’ll revisit this topic later in the chapter.
The call to example.metadata returns a hash containing all the metadata. We’re using the pp (short for pretty-print) function from Ruby’s standard library to dump the contents of the hash in an easy-to-read format.
Go ahead and run the example:
| $ rspec spec/metadata_spec.rb |
| {:block=> |
| #<Proc:0x007fa6fc07e6a8@~/code/metadata/spec/metadata_spec.rb:4>, |
| :description_args=>["is used by RSpec for metadata"], |
| :description=>"is used by RSpec for metadata", |
| :full_description=>"Hash is used by RSpec for metadata", |
| :described_class=>Hash, |
| :file_path=>"./spec/metadata_spec.rb", |
| :line_number=>4, |
| :location=>"./spec/metadata_spec.rb:4", |
| :absolute_file_path=> |
| "~/code/metadata/spec/metadata_spec.rb", |
| :rerun_file_path=>"./spec/metadata_spec.rb", |
| :scoped_id=>"1:1", |
| :execution_result=> |
| #<RSpec::Core::Example::ExecutionResult:0x007ffda2846a78 |
| @started_at=2017-06-13 13:34:00 -0700>, |
| :example_group=> |
| {:block=> |
| #<Proc:0x007fa6fb914bb0@~/code/metadata/spec/metadata_spec.rb:3>, |
| |
| truncated |
| |
| :shared_group_inclusion_backtrace=>[], |
| :last_run_status=>"unknown"} |
| . |
| |
| Finished in 0.00279 seconds (files took 0.09431 seconds to load) |
| 1 example, 0 failures |
Even before we’ve defined any metadata, RSpec has attached plenty of its own! Most of the keys in this hash are self-explanatory, but a few merit a closer look:
Just the string we passed to it; in this case, "is used by RSpec..."
Includes the text passed to describe as well; in this case, "Hash is used by RSpec..."
The class we passed to the outermost describe block; also available inside any example via RSpec’s described_class method
Directory and filename where the example is defined, relative to your project root; useful for filtering examples by location
Gives you access to metadata from the enclosing example group
Will be "passed", "pending", "failed", or "unknown"; the latter value appears if you haven’t configured RSpec to record the pass/fail status or if the example has never been run
As you’ll see in the coming sections, having this information at runtime will come in handy.
You can count on the keys in the previous section to be present in every metadata hash. Other keys may also be present, depending on metadata you’ve set explicitly. Update metadata_spec.rb to add a fast: true metadata entry:
| require 'pp' |
| |
| RSpec.describe Hash do |
» | it 'is used by RSpec for metadata', fast: true do |example| |
| pp example.metadata |
| end |
| end |
This particular style of use—passing a key whose value is true, as in fast: true—is so common that RSpec provides a shortcut. You can just pass the key by itself:
| require 'pp' |
| |
| RSpec.describe Hash do |
» | it 'is used by RSpec for metadata', :fast do |example| |
| pp example.metadata |
| end |
| end |
In either case, when you run this spec, you should see :fast=>true in the pretty-printed output.
You can even pass multiple keys:
| require 'pp' |
| |
| RSpec.describe Hash do |
» | it 'is used by RSpec for metadata', :fast, :focus do |example| |
| pp example.metadata |
| end |
| end |
You’ll see both :fast=>true and :focus=>true when you run this example.
Finally, when you set custom metadata on an example group, the contained examples and nested groups will inherit it:
| require 'pp' |
| |
» | RSpec.describe Hash, :outer_group do |
| it 'is used by RSpec for metadata', :fast, :focus do |example| |
| pp example.metadata |
| end |
| |
» | context 'on a nested group' do |
» | it 'is also inherited' do |example| |
» | pp example.metadata |
» | end |
» | end |
| end |
As you’d expect, these specs will print :outer_group=>true twice—once for the example in the outer group, and once for the example in the inner group.
Before we turn our attention to using metadata, let’s talk about one more way to set custom metadata.
As you’ve worked through the examples in this book, you’ve always set metadata on one example or group at a time. Sometimes, though, you want to set metadata on many examples at once.
For instance, you can mark your quickest-running examples as :fast, and then run just those specs using RSpec’s --tag option:
| $ rspec --tag fast |
This command would give you a quick overview of your code’s health. The set of fast specs would consist of certain hand-picked integration specs, plus everything in spec/unit (since your unit specs are meant to run quickly).
It’d be nice to not have to manually tag each example group in spec/unit with :fast. Fortunately, RSpec supports setting metadata on many examples or groups at once, via its configuration API.
If you add the following code to spec/spec_helper.rb:
| RSpec.configure do |config| |
| config.define_derived_metadata(file_path: /spec/unit/) do |meta| |
| meta[:fast] = true |
| end |
| end |
…RSpec will add the :fast metadata to every example in the spec/unit folder. Let’s break down how this code works.
RSpec’s define_derived_metdata method checks every example against the filter expression we give it. Here, the filter expression is file_path: /spec/unit/, which means, “match examples defined inside the spec/unit directory.”
When the filter expression matches, RSpec calls the passed block. Inside the block, we can modify the metadata hash however we like. Here, we’re adding fast: true to the metadata of every matching example. In effect, we’re filtering by one piece of metadata (:file_path) in order to set another one (:fast).
In this case, you used a regular expression to find all filenames containing spec/unit. At other times, you may need the values to match exactly. In the following snippet, we want to match all the specs tagged with type: :model to indicate they are testing our Rails models:
| RSpec.configure do |config| |
| config.define_derived_metadata(type: :model) do |meta| |
| # ... |
| end |
| end |
Behind the scenes, RSpec uses the === operator to compare values for your filter expression. You’ll hear more about this operator in How Matchers Match Objects. For now, we’ll just say that this style of comparison enables all kinds of things—like using a Ruby lambda in your filter expression. Most of the time, though, we find that regular expressions and exact matches are powerful enough.
In the previous section, you used metadata to tag some of the examples automatically. Now, we’ll do a twist on this concept: we’re going to show you how to enable something by default on all of your examples, but allow individual examples to opt out.
Back in Testing Ledger Behavior, you set the :aggregate_failures metadata on your integration specs to get more useful failure output. RSpec typically stops an example at the first failing expectation. With this tag in place, your integration specs would soldier on and report every failed expectation.
This aspect of RSpec is so useful that you may be tempted to enable it for every example:
| RSpec.configure do |config| |
| config.define_derived_metadata do |meta| |
| # Sets the flag unconditionally; |
| # doesn't allow examples to opt out |
| meta[:aggregate_failures] = true |
| end |
| end |
Switching on a feature globally can be extremely handy in situations like this. However, it’s a good idea to let individual examples opt out of the behavior.
For instance, you might have some billing specs that hit a fake payment gateway. As an extra safety check, you define a before hook that stops any spec attempting to use the real gateway:
| RSpec.describe 'Billing', aggregate_failures: false do |
| context 'using the fake payment service' do |
| before do |
| expect(MyApp.config.payment_gateway).to include('sandbox') |
| end |
| |
| # ... |
| end |
| end |
Even though aggregate_failures is set to false here, it’s getting overridden by the global setting. That means that if one of your examples is accidentally configured to talk to the real payment gateway (instead of the sandbox), the before hook won’t stop it.
The fix is easy: in your call to define_derived_metadata, check to see if the key exists first before overriding it:
| RSpec.configure do |config| |
| config.define_derived_metadata do |meta| |
| meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures) |
| end |
| end |
Now, you’re able to set the flag globally, but still switch it off for individual cases where you don’t want that behavior.
Next, let’s talk about how to access metadata and put it to good use.