Tim Sharpe

Home :: Projects :: About Me

Test Your Puppet Modules - Functions

1 11:00 <hubot> [puppet/master] merge branch 'mysql-module-refactor' - dave
2 11:00 <hubot> dave is deploying puppet/master to production
3 11:23 <nagios> PROBLEM - MySQL on dbmaster1.initech.com is CRITICAL
4 11:24 <dave> oh fuck

Look familiar?

If you've worked with configuration management systems for a while, a situation like this has probably cropped up and ruined your day. If you're lucky enough to have a homogeneous environment, then you might have a staging environment that you can test changes out on, but what if you don't? A slight mistake in that harmless change you're working on could stop a service on hundreds of machines or worse (purge the mysql-server package and all it's data *cough*).

Why you should be writing unit tests for your Puppet modules

  • Prevent situations like the one above.
  • Catch any problems moving between Puppet releases before it hits production.
  • Now we can do this too

Obligatory XKCD

Getting started

In this article, we'll cover setting up your testing environment and cover how to write unit tests for your Puppet functions. I'm going to make a few assumptions now:

  • You store your Puppet manifests in git.
  • You run a *nix machine as your workstation.
  • You don't mind getting your hands dirty with a bit of simple Ruby.
  • You have Ruby installed (1.8.7).

First of all, we're going to install Bundler to manage the dependencies our Puppet testing rig will have.

1 $ gem install bundler --no-ri --no-rdoc
2 Fetching: bundler-1.0.21.gem (100%)
3 Successfully installed bundler-1.0.21
4 1 gem installed
5 $ bundle --version
6 Bundler version 1.0.21

Next we're going to create a Gemfile in the top level of our Puppet repo with the list of gems we're going to need. Adjust the puppet and facter versions to match your environment.

1 source :rubygems
3 gem 'puppet',       '2.6.12'
4 gem 'facter',       '1.6.0'
5 gem 'rspec-puppet', '0.1.0'
6 gem 'rake',         '0.8.7'

Next we're going to add a couple of things to your .gitignore.

1 vendor/gems/
2 .bundle/

Now we just need to tell bundler to install everything and commit our changes to the repository.

 1 $ bundle install --path vendor/gems
 2 Fetching source index for http://rubygems.org/
 3 Installing rake (0.8.7)
 4 Installing diff-lcs (1.1.3)
 5 Installing facter (1.6.0)
 6 Installing puppet (2.6.12)
 7 Installing rspec-core (2.7.1)
 8 Installing rspec-expectations (2.7.0)
 9 Installing rspec-mocks (2.7.0)
10 Installing rspec (2.7.0)
11 Installing rspec-puppet (0.1.0)
12 Using bundler (1.0.21)
13 Your bundle is complete! It was installed into ./vendor/gems
14 $ git add Gemfile
15 $ git add Gemfile.lock
16 $ git commit -a -m "Bundler setup for testing"

OK, time to configure RSpec. Create a spec directory in the root of your Puppet repository and create a file in the spec folder called spec_helper.rb. Adjust c.module_path and c.manifest_dir to point to your modules and manifests directories in your repository.

1 require 'rspec-puppet'
3 RSpec.configure do |c|
4   c.module_path = "modules"
5   c.manifest_dir = 'manifests'
6 end

The last thing we need to do is create a Rake task to run our tests. Create a Rakefile in the root of your Puppet repository with the following.

1 require 'rake'
2 require 'rspec/core/rake_task'
4 RSpec::Core::RakeTask.new(:test) do |t|
5   t.pattern = 'modules/*/spec/*/*_spec.rb'
6 end
8 task :default => :test

Testing your first function

For the sake of this example, let's create a simple module with a single function (that I'm going to borrow from Puppet Lab's stdlib module).

1 $ mkdir -p modules/misc/lib/puppet/parser/functions
2 $ mkdir -p modules/spec/functions

Download this function and drop it in modules/misc/lib/puppet/parser/functions.

Basically this function should

  • return 0 if you pass it 'f', 'false', 'n', 'no', 0, '', 'undef' or 'undefined'.
  • return 1 if you pass it 't', 'true', 'y', 'yes' or 1.
  • raise Puppet::ParseError if you pass it anything else.

Before we continue, you should now go and read the rspec-puppet README. The tests for this function should live in modules/misc/spec/functions/bool2num_spec.rb.

 1 require 'spec_helper'
 3 describe 'bool2num' do
 4   it { should run.with_params('true').and_return(1) }
 5   it { should run.with_params('t').and_return(1) }
 6   it { should run.with_params('y').and_return(1) }
 7   it { should run.with_params('yes').and_return(1) }
 8   it { should run.with_params('1').and_return(1) }
10   it { should run.with_params('false').and_return(0) }
11   it { should run.with_params('f').and_return(0) }
12   it { should run.with_params('n').and_return(0) }
13   it { should run.with_params('no').and_return(0) }
14   it { should run.with_params('0').and_return(0) }
15   it { should run.with_params('undef').and_return(0) }
16   it { should run.with_params('undefined').and_return(0) }
17   it { should run.with_params('').and_return(0) }
19   it { should run.with_params('foo').and_raise_error(Puppet::ParseError) }
20 end

Most of this should be pretty self explanatory, however there is a couple of important things:

  • The spec_helper.rb required on line 1 is the spec_helper.rb in the spec directory at the root of your repository, not from the per-module spec directory.
  • The description on line 3 must be a string and it must be the name of the function that you are testing so that RSpec can set the subject correctly.

Now for the all important running of the tests.

1 $ bundle exec rake
2 /usr/bin/ruby -S rspec modules/misc/spec/functions/bool2num_spec.rb
3 ..............
5 Finished in 2.61 seconds
6 14 examples, 0 failures

Huzzah! Now go and write tests for the rest of your functions. In the next article in this series, we'll cover how to test your Puppet manifests (.pp files).