So now you have less errors and typos in your cookbooks, thanks to foodcritic. But you are still far from confident that your cookbook will not fail to run on some node. Next step for acquiring it is unit tests (aka specs in ruby world).
Ruby has already a great spec library useful for unit testing every kind of project - it’s called rspec. Many specialized unit test libraries are based on it and so is chefspec - the gem to write unit tests for your cookbooks.
Chefspec makes it easy to write unit tests for Chef recipes, get feedback fast on changes in cookbooks. So first let’s install it.
This will also add create_specs
command to knife
, which creates specs for particular existing
cookbook:
After this you will get a separate *_spec.rb
file in my_cookbook/specs/
for every recipe file.
Chefspec readme has very good examples teaching how to write tests. A couple of things I personally
do different is I use subject
and should
instead of let(:chef_run)
and expect(chef_run).to
,
because it allows to omit subject in some cases: (Read why
RSpec developers actually recommend using expect_to syntax)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Chefspec recommendations
describe "example::default" do
let( :chef_run ){ ChefSpec::ChefRunner.new.converge described_recipe }
it { expect(chef_run).to do_something }
it 'does some other thing' do
expect(chef_run).to do_another_thing
end
end
# My typical specs
describe "example::default" do
subject { ChefSpec::ChefRunner.new.converge described_recipe }
it { should do_something }
it 'does some other thing' do
should do_another_thing
end
end
We can also integrate it with Jenkins by making rspec output results in JUnit xml format that Jenkins understands. We need another gem for that:
Now we can run rspec with the following parameters and it will output test results into
test-results.xml
:
Rspec also supports rake, so it may be more convenient to use it to run specs on your cookbooks:
1
2
3
4
5
6
7
8
9
desc 'Runs specs with chefspec.'
RSpec::Core::RakeTask.new :spec, [:cookbook, :recipe, :output_file] do |t, args|
args.with_defaults( :cookbook => '*', :recipe => '*', :output_file => nil )
t.verbose = false
t.fail_on_error = false
t.rspec_opts = args.output_file.nil? ? '--format d' : "--format RspecJunitFormatter --out #{args.output_file}"
t.ruby_opts = '-W0' #it supports ruby options too
t.pattern = "cookbooks/#{args.cookbook}/spec/#{args.recipe}_spec.rb"
end