Getting started with RSpec Puppet

Written by | 15 minutes read | Tags ruby, howto | Comments

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.

Prevention of bad code with tests.

Prevention of bad code with tests.

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 perfect for creating a initial skeleton 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 https://github.com/andreashaerter/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. 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.rb
require 'spec_helper'

describe "git::init" do
  it { should create_class('git::packagee')}
end

Remember: 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 catalog does not contain a class git::packagee. Gosh, it’s a typo in our init_spec.rb file. Let’s fix this:

# spec/classes/init_spec.rb
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 an attribute

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 catalog 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 your lovely tests.

Further reading