Defining Metadata

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:

  • Example configuration (for example, marked as skipped or pending)
  • Source code locations
  • Status of the previous run
  • How one example runs differently than others (for example, needing a web browser or a database)

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.

Metadata Defined By RSpec

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:

:description

Just the string we passed to it; in this case, "is used by RSpec..."

:full_description

Includes the text passed to describe as well; in this case, "Hash is used by RSpec..."

:described_class

The class we passed to the outermost describe block; also available inside any example via RSpec’s described_class method

:file_path

Directory and filename where the example is defined, relative to your project root; useful for filtering examples by location

:example_group

Gives you access to metadata from the enclosing example group

:last_run_status

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.

Custom Metadata

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.

Derived 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.

Default Metadata

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset