ChefSpec should not test included recipe

Viewed 2922

I have built a cookbook for installing Jenkins CI. It uses the key and repository resources from the yum cookbook, so I end up with the following recipe:

yum_key "RPM-GPG-KEY-jenkins" do
  url "http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key"
  action :add
end

yum_repository "jenkins" do
  description "Jenkins-CI 3rd party repository"
  url "http://pkg.jenkins-ci.org/redhat"
  key "RPM-GPG-KEY-jenkins"
  action :add
end

When I include this recipe in another recipe:

include_recipe 'sp_jenkins::default'

and I test this with the following ChefSpec test

it 'includes the `sp_jenkins::default` recipe' do
  expect(chef_run).to include_recipe('sp_jenkins::install')
end

my ChefSpec test fails with the following output:

NameError:
  Cannot find a resource for yum_key on chefspec version 0.6.1

(I'm not sure why it says version 0.6.1, gem list tells me it's using 3.0.2)

The sp_jenkins cookbook does depend on the yum cookbook (metadata.rb), and runs fine, however, the cookbook I'm currently writing does not depend on the yum cookbook and therefore doesn't have the yum_key and yum_repository methods available.

Is there a way to prevent ChefSpec from 'descending' into included recipes/cookbooks and just test the current cookbook?

3 Answers

It is absolutely valid to expect to test your cookbook in isolation, and not include other projects' code into the scope of your tests. Unfortunately there appears to be no supported, "clean" way to do this, that I can find. I was able to achieve this, but it comes at a price.

To use this technique, do not require 'chefspec/berkshelf' anywhere in your test code, only chefspec itself, as you are intentionally not gathering other cookbook source. Here is a template of my working test module (not my complete test code, as I have omitted RSpec config options):

describe 'mycookbook::recipe' do
  let(:chef_run) do
    ChefSpec::SoloRunner.new(platform: 'x', version: 'x') {
      # ...
    }.converge(described_recipe)
  end

  before :each do
    allow_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:cookbook_order) do
      Chef::Log.debug 'Attempt to source external cookbooks blocked'
      [described_cookbook]
    end
    allow_any_instance_of(Chef::Recipe).to receive(:include_recipe) do |recipe|
      Chef::Log.debug "Attempt to include #{recipe} blocked"
    end
  end

  it 'works' do
    # ...
  end
end

You need both of these in your before. The one I had to work for is the intercept of the :cookbook_order method. I had to drill down into the Chef internals to discover this. Keep in mind, this worked for me using Chef 14, but there is no guarantee that this will be future-safe. After upgrading Chef you might have to find another solution, if the implementation of CookbookCompiler ever changes. (The intercept of Chef::Recipe.include_recipe however is a supported API and therefore should be at least somewhat future-safe.)

And, I mention that this comes at a price. (Other than using an unsupported hack!) You will not be able to do any expects for your recipe or attribute includes, except within your own cookbook. A test case like this will fail, because the recipe can't actually be included, as you are preventing that:

it 'includes othercookbook::recipe' do
  expect_any_instance_of(Chef::Recipe).to receive(:include_recipe).with('othercookbook::recipe')
end

Also, you must now satisfy in your before blocks all attributes and other preconditions that might otherwise be fulfilled by other recipes in your run list. So you may be signing yourself up for considerable pain by doing this. But, once you have finished, you will have much less brittle tests. (Although to achieve 100% purity regarding external dependencies, you must also surrender fauxhai, which will be even more painful.)

Related