You never improve if you can't change at all.
Tom DeMarco
Writing your service configuration with Puppet can be easy. But when it comes to debugging, it can be very difficult to edit things and to find certain errors. This article presents your the basic of setting up your environment for testing Puppet modules in a BDD way.
The tools
The rspec-puppet gem is the
Gem setup to get started. It install the rspec-puppet-init command
which automatically sets up the basic settings for testing. As a text tool we want to use
puppet-lint: This tool checks your Puppet manifests against the Puppet Labs
style guide and alert you to any discrepancies => you have now your constant feedback loop when writing code.
Run the following commands to install the gem:
$ gem install rspec-puppet
$ gem install puppet-lint
You can use puppet-lint in your terminal to check a puppet manifest.
$ puppet-lint <path-to-your-manifest>
Let’s assume, we have the following puppet file:
# manifests/init.pp
class git::init {
include git:: package
}
If we run puppet-lint on it:
$ puppet-lint manifests/init.pp
ERROR: two-space soft tabs not used on line 10
WARNING: unquoted resource title on line 10
Setting up the environment
The next step is to clone the puppet boilerplate repository. It’s perfekt for creating a initial skeletton for a new module . After we get the code, we run a script which guides you through the process of creating a new module:
$ git clone git://github.com/bitkollektiv/puppet-boilerplate-modules.git
$ ./puppet-boilerplate-modules/newmodule.sh
Answer the questions in this dialog, that means select the module name, the template for it (0: application-001 is perfect for the beginning), the location, and the author. When your are done with this, go into the directory of your new module and perform the following cleanup commands:
$ cd <your-module-path>
$ rm COPYING CREDITS Modulefile NOTICE README
$ rm -rf files/ templates/
The cleanup is necessary to get you focused on the basics of testing (of course documentation, README, and so on are important but not when you are going to learn something new). Now, your file structure should look like the following.
`-- manifests
|-- init.pp
|-- package.pp
`-- params.pp
This structure follows the package, config, and service (okay, we have params.pp instead of service.pp but this
not bad because the module we create in this example isn’t a service) pattern as mentioned by R.I.Pienaar blog
post.
The last step is to run rspec-puppet-init in the directory of your module and it will create all the files for
testing.
$ cd <your-module-path>
$ rspec-puppet-init
+ spec/
+ spec/classes/
+ spec/defines/
+ spec/functions/
+ spec/hosts/
+ spec/fixtures/
+ spec/fixtures/manifests/
+ spec/fixtures/modules/
+ spec/fixtures/modules/git/
+ spec/fixtures/manifests/site.pp
+ spec/fixtures/modules/git/manifests
+ spec/spec_helper.rb
+ Rakefile
And the file structure should be the following:
|-- manifests
| |-- init.pp
| |-- package.pp
| `-- params.pp
|-- Rakefile
`-- spec
|-- classes
|-- defines
|-- fixtures
| |-- manifests
| | `-- site.pp
| `-- modules
| `-- git
| `-- manifests -> ../../../../manifests
|-- functions
|-- hosts
`-- spec_helper.rb
The symlinks in the spec/ directory are linking the manifests folder into your spec folder, so that they are in the
runpath of your specs when you run rspec.
Testing, testing, and testing
First, we want to test that the class git::package is created in manifests/init.pp manifests. All we need to do is
to create a spec named init_spec.rb in the spec/classes directory. To make it testable, we need to define a scope
for our init.pp manifest
# manifests/init.pp
class git::init {
class { 'git::package': }
}
Let’s do the test for it:
# spec/classes/init_spec.pp
require 'spec_helper'
describe "git::init" do
it { should create_class('git::packagee')}
end
Now it’s time to run our test: (Remember to run rake spec always from the root directory of your module)
$ cd <your-module-path>
$ rake spec
/home/helex/.rbenv/versions/1.9.2-p320/bin/ruby -S rspec spec/classes/init_spec.rb
git::init
should contain Class[git::packagee] (FAILED - 1)
Failures:
1) git::init
Failure/Error: it { should create_class('git::packagee')}
expected that the catalogue would contain Class[git::packagee]
# ./spec/classes/init_spec.rb:4:in `block (2 levels) in <top (required)>'
Finished in 0.05637 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/classes/init_spec.rb:4 # git::init
Duh, it’s red, what should we do? The catalogue does not contain a class git::packagee. Gosh, it’s just a typo in our
init_spec.rb file. Let’s fix this:
# spec/classes/init_spec.pp
require 'spec_helper'
describe "git::init" do
it { should create_class('git::package')}
end
And run our tests again:
$ rake spec
/home/helex/.rbenv/versions/1.9.2-p320/bin/ruby -S rspec spec/classes/init_spec.rb
git::init
should contain Class[git::package]
Finished in 0.03963 seconds
1 example, 0 failures
It’s green and running – perfect.
Testing the creation of a package with a type
Since we are now sure, that the package manifests is integrated, it’s time to write a test, that we have the
git-core package in our package manifests. Let’s write spec/classes/install_spec.rb:
# spec/classes/package_spec.pp
require 'spec_helper'
describe 'git::package' do
context 'install git-core' do
it { should contain_package('git-core')
}
end
end
The contains_<resource> matcher will test if the manifest contains a particular puppet resource.
And run the tests again with rake spec form the root directory of your module:
$ rake spec
/home/helex/.rbenv/versions/1.9.2-p320/bin/ruby -S rspec spec/classes/package_spec.rb spec/classes/init_spec.rb
git::package
install git-core
should contain Package[git-core] (FAILED - 1)
git::init
should contain Class[git::package]
Failures:
1) git::package install git-core
Failure/Error: it { should contain_package('git-core')
expected that the catalogue would contain Package[git-core]
# ./spec/classes/package_spec.rb:6:in `block (3 levels) in <top (required)>'
Finished in 0.19843 seconds
2 examples, 1 failure
Failed examples:
rspec ./spec/classes/package_spec.rb:6 # git::package install git-core
Let’s edit manifests/package.pp file:
# manifests/package.pp
class git::package {
package { 'git-core':}
}
And run the tests again:
$ rake spec
/home/helex/.rbenv/versions/1.9.2-p320/bin/ruby -S rspec spec/classes/package_spec.rb spec/classes/init_spec.rb
git::package
install git-core
should contain Package[git-core]
git::init
should contain Class[git::package]
Finished in 0.19894 seconds
2 examples, 0 failures
Next we want to add the ensure attribute to the get the latest version of the git-core package. Let’s write a
failing test first:
# spec/classes/package_spec.pp
require 'spec_helper'
describe 'git::package' do
context 'install git-core' do
it { should contain_package('git-core')
.with_ensure('latest')
}
end
end
The with_* and without_* matcher can test the presence or absence of the parameter of resources. Run our tests, to
see they are failing:
$ rake spec
/home/helex/.rbenv/versions/1.9.2-p320/bin/ruby -S rspec spec/classes/package_spec.rb spec/classes/init_spec.rb
git::package
install git-core
should contain Package[git-core] with ensure => "latest" (FAILED - 1)
git::init
should contain Class[git::package]
Failures:
1) git::package install git-core
Failure/Error: .with_ensure('latest')
expected that the catalogue would contain Package[git-core] with ensure set to `"latest"` but it is set to `nil` in the catalogue
# ./spec/classes/package_spec.rb:7:in `block (3 levels) in <top (required)>'
Finished in 0.19862 seconds
2 examples, 1 failure
Failed examples:
rspec ./spec/classes/package_spec.rb:6 # git::package install git-core
Time to fix it:
# manifests/package.pp
class git::package {
package { 'git-core':
ensure => latest
}
}
If we run now our tests again, it should work:
$ rake spec
/home/helex/.rbenv/versions/1.9.2-p320/bin/ruby -S rspec spec/classes/package_spec.rb spec/classes/init_spec.rb
git::package
install git-core
should contain Package[git-core] with ensure => "latest"
git::init
should contain Class[git::package]
Finished in 0.20456 seconds
2 examples, 0 failures
If you have problems with understanding the syntax of RSpec, just checkout the “The RSpec Book” by David Chelimsky.
Refactor
Since we now have green tests, we can play with the code. Let’s make manifests.init.pp nicer:
# manifests/init.pp
class git::init {
# don't like the class declaration syntax: class { 'git::package': }
include git::package # much better
}
Run the test:
$ rake spec
/home/helex/.rbenv/versions/1.9.2-p320/bin/ruby -S rspec spec/classes/package_spec.rb spec/classes/init_spec.rb
git::package
install git-core
should contain Package[git-core] with ensure => "latest"
git::init
should contain Class[git::package]
Finished in 0.20158 seconds
2 examples, 0 failures
Your catalogue is still valid. Have beer because you have written your first tests for puppet and refactored your first manifest.
Conclusion
Testing is important – even or especially with the environment settings for your systems. It take some time to get used to it and you will find it in the beginning very cumbersome to write the code double. But when you are writing 4000 lines of code long manifest you will be happy to have structure, and confidence in your code with ya lovely tests.
Testing is good and saves your ass, especially the but of your company!
