Using Fake Cookbooks for Writing ChefSpec Tests for your Custom Chef Resources
In line with my article Test-Driven Chef Cookbook Development Using ChefSpec (and a sprinkling of InSpec), I heavily unit test the Custom Resources I write with my Chef code.
Until recently, the way that I've done this is creating a new recipe within the cookbook I'm testing, purely for the purpose of unit testing the resource.
For instance, let's assume I'm in a cookbook spectat
which has a resource called spectat_site
. In this case, I would create a recipe _spectat_site
whose purpose is purely to be used for unit testing the spectat_site
resource.
However, even when I adopted the pattern, I didn't like it - it means having numerous extra recipes within the cookbook that are technically available for external consumers to use, but shouldn't be, as they're just for driving tests. Additionally, that single recipe gets quite complicated so we can pass in all combinations of parameters and actions to ensure it's thoroughly tested.
I've since moved to the approach, stolen from the line cookbook, where I can have a "fake" cookbook purely for the sake of tests. This would mean I'd have an associated fake test cookbook called spectat_site
which has recipes for different aspects of testing the resource. This simplifies the logic within the resource-testing recipes, and makes intentions clearer to the reader.
Note that you do not need to have the fake cookbook named the same as the resource, I've used it in this example because, in my opinion, it makes it easier to see what the recipes are used for, but is up to you.
This fake cookbook sits in the spec/fixtures/cookbooks
folder, with the same name as the resource it's testing:
# spec/fixtures/cookbooks/spectat_site/metadata.rb
name 'spectat_site'
depends 'spectat'
It also needs a depends
on the cookbook we're testing, so it can then use the resources we want to test!
We can create the test within our top-level cookbook:
# spec/unit/resources/spectat_site/all_properties_spec.rb
describe 'spectat_site::all_properties' do
let(:chef_run) do
chef_run = ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '18.04', step_into: ['spectat_site'])
chef_run.converge(described_recipe)
end
it 'converges successfully' do
expect(chef_run).to_not raise_error
end
it 'creates the user' do
expect(chef_run).to create_user('create the user for www.jvt.me')
.with(username: 'www_jvt_me')
# ...
end
it 'creates the folder structure' do
expect(chef_run).to create_directory('create the directory for www.jvt.me')
.with(path: '/srv/www/www.jvt.me')
.with(owner: 'www_jvt_me')
.with(recursive: true)
# ...
end
# ...
end
Which has the following all_properties
recipe within our spectat_site
fake cookbook:
# spec/fixtures/cookbooks/spectat_site/recipes/all_properties.rb
spectat_site 'create a site for www.jvt.me' do
site_fqdn 'www.jvt.me'
site_type :static
end
Finally, to actually be able to find the new cookbook, we need to add to our Berksfile
:
source 'https://supermarket.chef.io'
metadata
+
+group :integration do
+ # TODO: add each new test cookbook here as needed
+ cookbook 'spectat_site', path: 'spec/fixtures/cookbooks/spectat_site'
+end
This then makes sure that just when testing, ChefSpec will know how to use that cookbook - awesome stuff!
As mentioned, this can be a really nice pattern for reducing complexity in resource-testing recipes, as well as remove the fake recipes from being used accidentally by our cookbook consumers.