Sharing Example Groups

As we’ve seen, plain old Ruby modules work really nicely for sharing helper methods across example groups. But that’s all they can share. If you want to reuse an example, a let construct or a hook, you’ll need to reach for another tool: shared example groups.

Just like its non-shared counterpart, a shared example group can contain examples, helper methods, let declarations, and hooks. The only difference is the way they’re used. A shared example group exists only to be shared.

In order to help you “get the words right,” RSpec provides multiple ways to create and use shared example groups. These come in pairs, with one method for defining a shared group and another for using it:

  • shared_context and include_context are for reusing common setup and helper logic.

  • shared_examples and include_examples are for reusing examples.

The choice between the ..._context and ..._examples wording is purely a matter of communicating your intent. Behind the scenes, they behave identically.

There’s one more way to share behavior that is different, though. it_behaves_like creates a new, nested example group to hold the shared code. The difference lies in how isolated the shared behavior is from the rest of your examples. In the coming sections, we’ll talk about when you’d want to use each approach.

Sharing Contexts

Earlier in this chapter, we saw that you can group common helper methods into a module:

 module​ APIHelpers
 include​ Rack::Test::Methods
 
 def​ app
  ExpenseTracker::API.new
 end
 end

This technique works fine as long as you’re only dealing with helper methods. Sooner or later, though, you’ll find that you want to share some let declarations or hooks instead.

For example, you may want to add authentication to your API. After you’ve done so, you’ll need to modify your existing specs to log in before they make their requests. Since being logged in is an extraneous detail for most of your specs, a before hook would be the perfect place to put this new behavior:

 before​ ​do
  basic_authorize ​'test_user'​, ​'test_password'
 end

Here, we’re using the basic_authorize method from Rack::Test to treat the HTTP request as coming from a logged-in test user.

This hook can’t go into your APIHelpers module, though. Plain Ruby modules aren’t aware of RSpec constructs such as hooks. Instead, you can convert your module to a shared context:

 RSpec.shared_context ​'API helpers'​ ​do
 include​ Rack::Test::Methods
 
 def​ app
  ExpenseTracker::API.new
 end
 
 before​ ​do
  basic_authorize ​'test_user'​, ​'test_password'
 end
 end

To use this shared context, you’d just call include_context from any example group that needs to make API calls:

 RSpec.describe ​'Expense Tracker API'​, ​:db​ ​do
  include_context ​'API helpers'
 
 # ...
 end

With include_context, RSpec evaluates your shared group block inside this group, causing it to add the hook, helper method, and module inclusion here. As with include, you can also use it in an RSpec.configure block for those rare situations where you want to include the shared group in all example groups:

 RSpec.configure ​do​ |config|
  config.include_context ​'API helpers'
 end

Next, let’s turn to the other common use case for shared example groups: sharing examples instead of context.

Sharing Examples

One of the most powerful ideas in software is defining a single interface with multiple implementations. For example, your web app might need to cache data in a key-value store.[56] There are many implementations of this idea, each with its own advantages over the others. Because they all implement the same basic functionality of kv_store.store(key, value) and kv_store.fetch(key), you can choose the implementation that best fits your needs.

You don’t even have to pick just one implementation. You might use one key-value store for production and a different one for testing. In production, you’re likely to want a persistent store that keeps data around between requests. For testing, you can save time and complexity by using an in-memory key-value store.

If you’re using more than one key-value store, it’s important to have some confidence that they have the same behavior. You can write specs to test this behavior, and organize them using shared examples.

Without shared examples, you might start with the following spec for an in-memory HashKVStore:

 require ​'hash_kv_store'
 
 RSpec.describe HashKVStore ​do
 let​(​:kv_store​) { HashKVStore.new }
 
 it​ ​'allows you to fetch previously stored values'​ ​do
  kv_store.store(​:language​, ​'Ruby'​)
  kv_store.store(​:os​, ​'linux'​)
 
 expect​(kv_store.fetch(​:language​)).to eq ​'Ruby'
 expect​(kv_store.fetch(​:os​)).to eq ​'linux'
 end
 
 it​ ​'raises a KeyError when you fetch an unknown key'​ ​do
 expect​ { kv_store.fetch(​:foo​) }.to raise_error(KeyError)
 end
 end

To test a second implementation of this interface—such as a disk-backed FileKVStore—you could copy and paste the entire spec and replace all occurrences of HashKVStore with FileKVStore. But then you’d have to add any new common behavior to both spec files. We’d have to manually keep the two spec files in sync.

This is exactly the kind of duplication that shared example groups can help you fix. To make the switch, move your describe block into its own file in spec/support, change it to a shared_examples block taking an argument, and use that argument in the let(:kv_store) declaration:

»RSpec.shared_examples ​'KV store'​ ​do​ |kv_store_class|
»let​(​:kv_store​) { kv_store_class.new }
 
 it​ ​'allows you to fetch previously stored values'​ ​do
  kv_store.store(​:language​, ​'Ruby'​)
  kv_store.store(​:os​, ​'linux'​)
 
 expect​(kv_store.fetch(​:language​)).to eq ​'Ruby'
 expect​(kv_store.fetch(​:os​)).to eq ​'linux'
 end
 
 it​ ​'raises a KeyError when you fetch an unknown key'​ ​do
 expect​ { kv_store.fetch(​:foo​) }.to raise_error(KeyError)
 end
 end

By convention, shared examples go in spec/support; we’ve named this file kv_store_shared_examples.rb after the common interface we’re testing.

The block argument, kv_store_class, will come from the calling code (which we’ll see in a moment). Here, it represents the class we’re testing.

Conventions Are There for a Reason

images/aside-icons/info.png

When we suggest names and locations for support files, we’re not just being fussy. Choosing the right name can help you avoid errors. We’ve seen people name their support files something like shared_spec.rb—which RSpec will attempt to load as a regular spec file, resulting in warning messages.

Now, you can replace your original spec file with a much simpler one:

 require ​'hash_kv_store'
 require ​'support/kv_store_shared_examples'
 
 RSpec.describe HashKVStore ​do
  it_behaves_like ​'KV store'​, HashKVStore
 end

We’re explicitly passing the HashKVStore implementation class when we bring in the shared examples with it_behaves_like. The shared_examples block from the previous snippet uses this class in its let(:kv_store) declaration.

Nesting

In the introduction to this section, we mentioned that you can include shared examples with either include_examples or it_behaves_like call. So far, we’ve just used it_behaves_like. Let’s talk about the difference between the two calls. Here’s a version of the snippet with include_examples instead:

 RSpec.describe HashKVStore ​do
  include_examples ​'KV store'​, HashKVStore
 end

The spec would behave just fine, but problems creep in if we add a second call to include_examples:

 RSpec.describe ​'Key-value stores'​ ​do
  include_examples ​'KV store'​, HashKVStore
  include_examples ​'KV store'​, FileKVStore
 end

Calling include_examples is like pasting everything in the shared example group directly into this describe block. In particular, you’d get two let declarations for :kv_store: one for HashKVStore and one for FileKVStore. One would overwrite the other. The documentation output shows how they are stepping on each other’s toes:

 $ ​​rspec spec/include_examples_twice_spec.rb --format documentation
 
 Key-value stores
  allows you to fetch previously stored values
  raises a KeyError when you fetch an unknown key
  allows you to fetch previously stored values
  raises a KeyError when you fetch an unknown key
 
 Finished in 0.00355 seconds (files took 0.10257 seconds to load)
 4 examples, 0 failures

Using it_behaves_like avoids this issue:

 RSpec.describe ​'Key-value stores'​ ​do
  it_behaves_like ​'KV store'​, HashKVStore
  it_behaves_like ​'KV store'​, FileKVStore
 end

Here, each example group gets nested into its own context. You can see the difference when you run with the --format documentation option to RSpec:

 $ ​​rspec spec/it_behaves_like_twice_spec.rb --format documentation
 
 Key-value stores
  behaves like KV store
  allows you to fetch previously stored values
  raises a KeyError when you fetch an unknown key
  behaves like KV store
  allows you to fetch previously stored values
  raises a KeyError when you fetch an unknown key
 
 Finished in 0.00337 seconds (files took 0.09726 seconds to load)
 4 examples, 0 failures

Because each use of the shared example group gets its own nested context, the two let declarations don’t interfere with each other.

When in Doubt, Choose it_behaves_like

images/aside-icons/info.png

Wondering about which method to use to include your shared examples? it_behaves_like is almost always the one you want. It ensures that the contents of the shared group don’t “leak” into the surrounding context and interact with your other examples in surprising ways.

We recommend using include_examples only when you’re sure the shared group’s context won’t conflict with anything in the surrounding group, and you have a specific reason to use it. One such reason is clarity: sometimes, your spec output (using the documentation formatter) will read more legibly without the extra nesting.

Customizing Shared Groups With Blocks

As these examples have shown, you can customize your shared example groups’ behavior by passing an argument into the it_behaves_like block. This technique works fine when all you need to do is pass in a static argument. Sometimes, though, you need more flexiblity than that.

In our examples so far, we’ve passed the class we’re testing—HashKVStore or FileKVStore—as a static argument when we include the group. The shared code just calls new on the passed-in class to create a new store.

If these classes require arguments for instantation, passing an argument is not going to work. For example, a file-based key-value store may require you to pass in a filename as an argument.

Fortunately, you have one more trick up your sleeve: you can pass a block (instead of just an argument) to your it_behaves_like call:

 require ​'tempfile'
 
 RSpec.describe FileKVStore ​do
  it_behaves_like ​'KV store'​ ​do
 let​(​:tempfile​) { Tempfile.new(​'kv.store'​) }
 let​(​:kv_store​) { FileKVStore.new(tempfile.path) }
 end
 end

With this technique, your block can contain whatever RSpec constructs you need. Here, we’ve used a let definition, but you can also also add helper methods and hooks inside the same kind of block.

To make this technique work, you’d need to change the definition of the KV store shared example group.

»RSpec.shared_examples ​'KV store'​ ​do
 it​ ​'allows you to fetch previously stored values'​ ​do
  kv_store.store(​:language​, ​'Ruby'​)
  kv_store.store(​:os​, ​'linux'​)
 
 expect​(kv_store.fetch(​:language​)).to eq ​'Ruby'
 expect​(kv_store.fetch(​:os​)).to eq ​'linux'
 end
 
 # remainder of examples...
 end

The shared group no longer needs to take a block argument or define let(:kv_store). It just uses kv_store normally inside each example, trusting that the host group will define it with the right value for the context.

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

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