Testing include_recipes with Chef and ChefSpec

Featured image for sharing metadata for article

While writing cookbooks, both personally and professionally, I practice a heavy use of TDD to ensure that the recipes are doing what I expect them to. As part of this, I will want to test both standard resources, as well as include_recipes:

describe 'cookbook::default' do
  let(:chef_run) do
    # ...
  end

  it 'includes the ::user recipe' do
    expect_any_instance_of(Chef::Recipe).to receive(:include_recipe).with('cookbook::user')
    chef_run
  end

  it 'converges successfully' do
    expect { chef_run }.to_not raise_error
  end
end

Ensuring dependent recipes don't get run

When you're performing a runner.converge with ChefSpec, it is performing a converge by going through each of the recipes and running them in-memory. Because it actually runs the recipe, it means that if a given recipe requires any attributes to be set, then you will also need to put your attributes into the calling recipe. As I'm sure you can guess, having recipes including each other will then start to have quite a large set of attributes and configuration required in order to test what looks like a single recipe, but is instead the full chain of recipes required. This breaks the idea of "unit testing", as it doesn't give us a single unit to test against.

Therefore, the best way to get around this, is to simply not let the other recipes be run. Taking advantage of RSpec Mocks, we effectively let include_recipe be called, but it doesn't do anything:

describe 'cookbook::default' do
  let(:chef_run) do
    # ...
  end

  before :each do
    allow_any_instance_of(Chef::Recipe).to receive(:include_recipe)
      .with('cookbook::user')
  end

  it 'includes the ::user recipe' do
    # ...
end

Defensive include_recipes

However, if we have this running, it won't flag up include_recipe being called on any other recipes that we've not predicted in our tests. Yes, this should be more obvious when practicing TDD, but it still doesn't actual fail our tests if we're not catching anything. This would mean that recipes could be silently executing in the background, slowing down tests, which may not be as noticeable in the case that they don't require any extra attributes set.

To do this, we can utilise RSpec Mocks again, but this time, we can raise if there's a non-whitelisted recipe called.

describe 'cookbook::default' do
  let(:chef_run) do
    # ...
  end

  before :each do
    allow_any_instance_of(Chef::Recipe).to receive(:include_recipe).and_raise('include_recipe not matched')
    allow_any_instance_of(Chef::Recipe).to receive(:include_recipe)
      .with('cookbook::user')
  end

  it 'includes the ::user recipe' do
    # ...
  end
end

Note the order of precedence - this is the catch-all, and then each recipe we want to whitelist is overriding the mock.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #chef #tdd #chefspec.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.