Sharing Serverspec examples

Following on from last weeks post, this post is going to go over sharing Serverspec examples between multiple hosts.

Default setup

After running serverspec-init for a few hosts you will end up with a folder structure that looks something like this:

.
+-- Rakefile
+-- spec
    +-- remote-a.example.com
    ¦   +-- sample_spec.rb
    +-- remote-b.example.com
    ¦   +-- sample_spec.rb
    +-- remote-c.example.com
    ¦   +-- sample_spec.rb
    +-- spec_helper.rb

When you run rake spec the following happens:

  1. rake parses the Rakefile and runs the :spec task.

  2. The :spec task finds all of the directories under the spec directory and creates a task for each host:

    RSpec::Core::RakeTask.new(target.to_sym) do |t|
      ENV['TARGET_HOST'] = original_target
      t.pattern = "spec/#{original_target}/*_spec.rb"
    end
    
  3. Each host task then invokes rspec to run the spec files for each host.

  4. Finally spec files will load spec_helper.rb and declare the tests which rspec should run.

This is fine if you only have a handful of hosts, however if you have hundreds of hosts, duplicate test code will quickly become a problem.

Creating shared examples

Shared examples will look something like this:

shared_examples 'webserver' do

  describe package('httpd') do
    it { should be_installed }
  end

  describe service('httpd') do
    it { should be_enabled }
    it { should be_running }
  end

  describe port(80) do
    it { should be_listening }
  end
end

For this post, shared examples are going to be kept in a directory called spec/shared/. So the code above would be saved in spec/shared/webserver.rb; however feel free to use an alternative directory.

Note: you can include multiple shared examples in a single file, however sticking to one file per shared example makes the examples easier to find and manage.

Because the shared directory is in the spec directory it will be picked up by the :spec task. This can be fixed by making the following change to the rakefile.

 namespace :spec do
   targets = []
   Dir.glob('./spec/*').each do |dir|
     next unless File.directory?(dir)
+    next if File.basename(dir) == 'shared'
     target = File.basename(dir)
     target = "_#{target}" if target == "default"
     targets << target
   end

Before the shared examples can be used, they need to be imported by each spec file. This can be done easily by adding the following lines to the spec_helper.rb file:

# Load shared examples
base_spec_dir = Pathname.new(File.join(File.dirname(__FILE__)))
Dir[base_spec_dir.join('shared/**/*.rb')].sort.each{ |f| require f }

Finally once shared examples are loaded, they can be included in host spec files:

$ cat spec/remote-a.example.com/main_spec.rb
require 'spec_helper'

describe 'remote-a.example.com' do
  include_examples 'webserver'
end

$ cat spec/remote-b.example.com/main_spec.rb
require 'spec_helper'

describe 'remote-b.example.com' do
  include_examples 'webserver'
end