When it comes to testing your Chef cookbooks, currently there seem to be two tools that build the standard in the Chef community: ChefSpec and Test Kitchen (with Serverspec, which is the third tool). Where Serverspec can be used for an outside-in testing approach for your infrastructure, ChefSpec is more like unit testing or input testing for your Chef cookbooks. In this blogpost I’ll try to provide some best practices and snippets for common use cases.
ChefSpec’s goal is to simulate a convergence run of Chef in-memory and test expected behavior against this in-memory results. Due to only converging the node in-memory, it provides fast feedback thus making an ideal test framework for a commit stage in a Continuous Integration pipeline for your cookbooks.
ChefSpec Setup
Let’s assume you have a cookbook structure like this:
In order to use ChefSpec in your cookbook, make sure to add it to your Gemfile
After you created your Gemfile, run bundle install to install all dependencies.
The spec_helper.rb configures the rspec framework and looks like this:
All you have to do to run ChefSpec is to go to the root of your cookbook and run rspec:
Writing your first Spec
Given the following recipe that you want to write a spec for:
require'spec_helper'describe'app_virtualbox::default'dovirtual_box_version='5.0'let(:chef_run)doChefSpec::SoloRunner.newdo|node|node.set['virtualbox']['version']=virtual_box_versionend.converge(described_recipe)endit'adds the expected apt repository'doexpect(chef_run).toadd_apt_repository('oracle-virtualbox').with(:uri=>'http://download.virtualbox.org/virtualbox/debian',:key=>'http://download.virtualbox.org/virtualbox/debian/oracle_vbox.asc',:distribution=>'trusty',:components=>['contrib'])endit'installs expected apt package for VirtualBox'doexpect(chef_run).toinstall_package("virtualbox-#{virtual_box_version}")endit'installs expected apt package for dkms'doexpect(chef_run).toinstall_package('dkms')endend
In line 3 we open a describe block for the recipe under test (app_virtualbox::default). Line 7 simulates the Chef run - in memory, as I described before - and “overrides” a few node attributes that we’ll use in our specs later.
Line 14 shows how we can assert that resources have been invoked by our Chef run. The add_apt_package method is a so-called matcher, which is defined for every resource that ships with Chef in the ChefSpec framework (see the extensive example folder on Github for a complete list of default matchers and their usage). Additional matchers can either be defined in your own cookbooks or community cookbooks or can be added to your specs for those cookbooks, which do not define their own matchers as we’ll see later.
Using the with method in line 114, you can further specify the attributes that you want the resource to be called with.
Running this spec, the output will look like this (given you use the parameters -f d -c when calling rspec):
“Advanced” Use Cases
Mocking Recipe Inclusion
Let’s say you have a recipe that includes two other recipes:
and you want to test whether your recipe includes the expected recipes (and only those), then you can use the following spec to do this:
Mocking Data Bags
If your recipe under test uses a data bag:
you can mock them as well:
Mocking Shell Commands (e.g. for not_if conditions)
Given you use a shell command in a not_if attribute in your resource:
and you want to stub this command to either return true or false in your spec, use this snippet:
Mocking File.exists? and IO.read
Let’s assume, you want to test a recipe like this:
Therefore, you have to figure out, how to mock a call to File.exists? and IO.read in order to mock out the behavior and check whether an exception is raised in the expected circumstances. You can do that with this before block:
Notifications to other resources
Let’s assume we have a service resource for the Apache server like this
and another resource that writes a configuration file, which should effect a restart of the Apache server
then you can use the following Chefspec test to check whether the notification happens:
Testing for raised exceptions during Chef run
Assume you have a recipe code like this:
Then you can check for the exception being raised in your Chef run using
Write Custom Matchers
If you are using a 3rd-party cookbook that does not provide a matcher for a resource defined within this cookbook, you can write your own matcher. Let’s say you have this resource in your recipe (it actually doesn’t provide a matcher!):
then you can write a matcher ssh_known_hosts_entry_matcher.rb file like this:
and include it in your spec_helper.rb like that:
If you have several matchers that you have to implement on your own, you might come up with a folder structure like this:
and you can use the following snippet in your spec_helper.rb to include those matchers:
Using ChefSpec with Berkshelf
In case you are using Berkshelf for cookbook dependency management, simply add the following line to your spec_helper.rb:
ChefSpec will then automatically resolve cookbook dependencies for you.
Test Coverage
The following snippet in your spec_helper.rb file will switch test coverage on:
In your rspec output you will then find something like this:
The .rspec File
You can add a file .rspec in your cookbook’s root directory that holds a default configuration for your rspec test runs:
this will result in a colored output with the decriptions in the describe and it blocks rendered as output on the shell.
Rake Helper Task
A simple Rake task can help you to run all ChefSpec tests in a subdirectory at once. Let’s assume that your coobooks are stored in a subfolder cookbooks: