Testing your cookbooks with test-kitchen + chefspec + serverspec, part 2

Cookbooks can also get the same red, green, refactor loop going the same way you would with a simple application. It's not all roses though because the loop itself takes a few minutes at a time. Even with a simple cookbook, you're looking at 3-4 minutes each run if you're constantly testing using converges.

In terms of the fastest feedback loops, you should:

  • Use Chefspec to test that you're telling chef the correct things to do. These will run in a few seconds.
  • Use Serverspec that chef did what you told it to. This will take 3-4 minutes using a container.

Adding Chefspec

spec/spec_helper.rb

require 'chefspec'

RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    mocks.syntax = :expect
  end

  config.platform = 'ubuntu'
  config.version = '14.04'
end

spec/recipes/default_spec.rb

require 'spec_helper'

describe 'hello-world-cookbook::default' do
  let(:chef_run) {
    ChefSpec::SoloRunner.new.converge(described_recipe)
  }

  it 'installs a file' do
    expect(chef_run).to create_file('/tmp/foobar')
  end
end

Run bundle exec rspec spec to start executing Chefspec tests. You'll see the failing test. Fix it by updating recipes/default.rb to create a file with content and watch it go green.

How not to use ChefSpec

it 'creates file' do
  expect(File.new('/tmp/foobar').exists?).to be_truthy
end

This won't work because the chef run only happens in memory as part of chefspec. No changes are actually made to the system. If you're running rspec from your terminal, then the chef run happens in memory on your system on your current OS. When you want to do this type of testing, it's easier to test this stuff in Serverspec.

Adding Serverspec

Add the following files

test/integration/default/serverspec/spec_helper.rb

require 'serverspec'

set :backend, :exec

test/integration/default/serverspec/default_spec.rb

require 'spec_helper'

describe file('/tmp/foobar') do
  it { should be_readable.by_user('root') }
  it { should contain('foo')}
end

Now run bundle exec kitchen verify. test-kitchen will update the container with the new cookbook, execute the cookbook inside the container, upload the test runner inside the container, and then execute the tests. This is a great way to test modifications to the system like if files exist with the correct permissions.

Lint your cookbook

Add rubocop & foodcritic to your project by adding it to your Gemfile,

gem 'rubocop'
gem 'foodcritic'

Rubocop will sniff out smelly parts of your ruby. It occasionally gives you good advice and can be helpful to keep consistent styling. It does need to be tuned lest it constantly complain about long lines.

Run 'bundle exec rubocop' to see what it complains about out of the box. Things like Style/FrozenStringLiteralComment are just not useful.

Foodcritic will look for Chef specific errors in your cookbook. Like if you meant to render a template but forgot to include the template file.

Some of the stuff it complains about I don't care for, so you can always ignore some by adding --tags ~RULE# to your command. You can see what it complains about out of the box by running bundle exec foodcritic ./.

Most of its initial complaints are about fields you should have in your metadata to be a good Chef citizen publishing open-source cookbooks. If you're using them in a private setting, then its just boilerplate.

You've now got an entire development flow for your configuration.

  • Build the cookbook by running kitchen converge
  • Unit test your Chef logic by running rspec spec with Chefspec
  • Run an Integration Test by actually running Chef on the target container
  • Lint your cookbook to keep it close to ruby best practices and to catch obvious Chef errors.

Stay tuned for Part 3 where we tie this into a CI workflow.

You might be interested in…

Menu