ChefSpec should not test included recipe

3k views Asked by At

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

There are 3 answers

1
sethvargo On BEST ANSWER

Ohai! Julian is correct - ChefSpec actually does a Chef Solo run in memory on your local machine. It rewrites the provider actions to be a noop, but creates a registry of all the actions taken (including those that would be taken if notifications were executed).

So just like you need the yum cookbook to converge this recipe on a real node, you need it to converge during your unit tests with ChefSpec. The easiest way to accomplish this is by using the Berkshelf or Librarian resolvers. To use the Berkshelf resolver, simply require 'chefspec/berkshelf' after requiring chefspec:

# spec_helper.rb
require 'chefspec'
require 'chefspec/berkshelf'

If you have Berkshelf installed on your system, it will pull all the cookbooks into a temporary directory and run ChefSpec for you.

You may also want to take a look at Strainer, which aims to solve a similar problem.


On a somewhat unrelated note, I am working on a fairly large refactor to the Jenkins cookbook that may better suit your needs.


Sources:

  • I wrote it...
0
Julian Dunn On

No, there's no way to prevent it from descending, because it's trying to converge an entire Chef run in memory.

However, if you use the Berkshelf functionality in ChefSpec, the Berkshelf dependency resolver will feed all dependent cookbooks to the in-memory Chef run, and you'll be golden.

0
wberry On

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