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
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.
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 cookbook '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
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.