During the last few months we've been heavily adding unit testing to our Chef codebase. In this article I will explain the benefits and how to use shared_examples and shared_context in ChefSpec testing.

ChefSpec isn't anything more than an extension of RSpec for unit infrastructure testing. We have all the resources available to express assertions that RSpec provides, I will focus on two of them.

Shared Examples

According to the official documentation:

Shared examples let you describe behaviour of types or modules. When declared, a shared group's content is stored. It is only realized in the context of another example group, which provides any context the shared group needs to run.

You can use a shared example to group common assertions that can be reused for different use cases in your test suite. There are two relevant pieces of syntax used:

  • To define the shared example you just use shared_examples and pass a block.
  • To include a shared example in your testing you can use one of the following keywords:
     include_examples 'name'      # include the examples in the current context
     matching metadata            # include the examples in the current context
     it_behaves_like 'name'       # include the examples in a nested context
     it_should_behave_like 'name' # include the examples in a nested context
    

Let's try a small example: assume that all of your servers require a few base packages installed. For the sake of this example let's say that it's just htop and nmap. But your web servers also require postgresql-9.5 and your database servers also require postgresql.


shared_examples 'base server' do
  it 'has htop package installed' do
    expect(chef_run).to install_apt_package('htop')
  end

  it 'has nmap package installed' do
    expect(chef_run).to install_apt_package('nmap')
  end
end

describe 'mycookbook::web' do
  let(:chef_run) do
     ChefSpec::SoloRunner.converge(described_recipe)
   end

  it_behaves_like 'base server'
 
  it 'has nginx package installed' do
    expect(chef_run).to install_apt_package('nginx')
  end  
end

describe 'mycookbook::database' do
  let(:chef_run) do
     ChefSpec::SoloRunner.converge(described_recipe)
   end

  it_behaves_like 'base server'
 
  it 'has postgresql-9.5 package installed' do
    expect(chef_run).to install_apt_package('postgresql-9.5')
  end  
end

As the example illustrates, this is a good way of expressing common behavior for your test suite. Go to the official RSpec documentation for a complete explanation.

Shared Context

We head back to the official definition for the current explanation for a shared context:

Use shared_context to define a block that will be evaluated in the context of example groups either locally, using include_context in an example group, or globally using config.include_context.

We use shared context to reuse an inital state of the test that different sets of assertions can use later.

To declare a shared context you need two keywords:

  • Use shared_context and pass a block to declare the context.
  • Use include_context to include the context in a block.

For our example we'll assume that our tests for different recipes need a common set of Chef node attributes, a databag, and to memorize the value of an FQDN.

shared_context 'test_server' do
  let(:fqdn) { 'srv0.example.com' }
  runner = ChefSpec::ServerRunner.new do |node, server|
    node.normal['nginx']['version'] = '1.2.3'
    node.automatic['network']['default_interface'] = 'eth0'
    server.create_data_bag('network', { 'eth0' => '192.168.1.2' })
    server.update_node(node)
  end
  runner.converge(described_recipe)
end

Then we can reuse this series of settings in different group of tests


describe 'mycookbook::web' do
  include_context 'test_server'

  ...
end

describe 'mycookbook::database' do
  include_context 'test_server'

  ...
end

There is an alternative way of declaring shared contexts via matching metadata. You can learn more about shared contexts by going to the official docs.

Summary

Shared context and examples are a good, DRY way of reusing useful information for your tests and to reuse the tests themselves. You can take advantadge of both to have a cleaner, more efficient, and wider test suite.