Unit Testing With rspec-puppet for Beginners
Unit testing, even for Puppet code, is important. Testing allows you the peace of mind of knowing that even minor changes will not break compilation of the catalog.
Join the DZone community and get the full member experience.
Join For FreeSo, you need to implement rspec testing of your Puppet modules. Where do you start? Why is rspec-puppet testing important? How is this going to help your code? Testing allows you the peace of mind of knowing that even minor changes in your Puppet code will not break compilation of the Puppet catalog.
I recently found myself in this same situation, starting with rspec-puppet testing. This post documents the struggles, pitfalls, and shortcuts I learned along the way. By no means am I an expert on rspec-puppet testing, and in fact, I still have a lot to learn. However, I'd like to offer what I've learned as a beginner to help you if you, too, are just starting out with rspec-puppet testing.
First off, the main question I had was where to start. You have a Puppet module you have inherited, or just written yourself, so how do you get this rspec-puppet thing going to be able to test your modules as Puppet recommends?
Instead of jumping directly into testing your modules, let's start with the basics needed to begin your testing. You'll need several Ruby gems. You can use Ruby Version Manager (RVM) or your preferred Ruby environment manager to install them.
- gem install puppet.
- gem install puppet-lint.
- gem install puppet-syntax.
- gem install puppetlabs-spec-helper.
OK, that’s done. What next? Now, we need to learn some basics about the directory structure of your rspec-puppet testing. We are not going into how to use version control tools like Git or SVN; will assume you already know how to do that, or have a local copy of the module that you are working on. The typical directory structure of your module looks something like this:
module_name/
├── examples
├── files
├── lib
├── manifests
│ └── init.pp
├── spec
└── templates
Your rspec-puppet test files reside inside your module_name/spec
directory, and should have a structure similar to this:
spec/
├── classes
├── defines
└── fixtures (don’t worry about creating this directory as the first time you run `rake spec`, it will create it for you)
Now, there could be other directories, also, but for all intents and purposes of this post, this is all we will cover. There are three other files that are required for all this to come together:
/.fixtures.yml
: Describes dependency modules./Rakefile
: Definition of Rake tasks./spec/spec_helper.rb
: Specifications for the rspec-puppet tasks.
.fixtures.yml
:
fixtures:
symlinks:
nginx: "#{source_dir}"
repositories:
pe_staging: "git@github.com:puppetlabs/puppetlabs-pe_staging.git"
forge_modules:
stdlib:
repo: "puppetlabs/stdlib"
ref: "4.12.0"
Rakefile:
require ‘puppetlabs_spec_helper/rake_tasks’
spec_helper.rb
:
require ‘puppetlabs_spec_helper/module_spec_helper’
With these in place, you are now ready to start writing your spec tests. Remember the classes
and defines
directories inside the module_name/spec
directory? As you might expect, classes go inside classes
and defined types go inside defines
. Let’s get started with a simple class file to start testing, something like this:
init.pp
:
class nginx {
package { ‘nginx’:
ensure => present,
}
file { ‘/var/www/index.html’:
ensure => file,
require => Package[‘nginx’],
}
service { ‘nginx’:
ensure => running,
enable = true,
}
}
In this simple class, we install the Nginx package, ensure the presence of the index.html
, and make sure the service is running and enabled. We know we can do both a puppet parser validate
and a puppet-lint
to test the syntax and format, but what about unit testing? This is where rspec-puppet comes into play.
Our class file for testing will have a name similar to init_spec.rb
, which represents the name of the class (in this case, init
) and with the suffix _spec.rb
, letting us know that it is a Ruby file specifically for spec testing.
init_spec.rb
:
require ‘spec_helper’
describe 'nginx' do
end
This is the most basic file to start with, requiring the spec_helper
that we referenced earlier, and we are going to describe the test we are going to run. Between the do
and the end
is where our unit test code goes. Before we get to our code, there are some parameters we can set up for the tests. They look like this:
let(:title) { 'nginx' } # this does exactly what you think - sets the title of the class declaration
let(:node) { 'test.example.com' } # this sets a node name if needed
let(:facts) { {
:fact1 => 'value 1',
:fact2 => 'value 2',
} } # this is where you can define facts for your tests like `osfamily`, `architecture`, `kernel`, etc.
let(:params) { { } } # defined much like the facts, these are parameters that your class accepts
class nginx (
$docroot, # This parameter can be passed in from Hiera or classification and can be tested with `let(:params)`
) {
package { ‘nginx’:
ensure => present,
}
file { $docroot:
ensure => directory,
}
file { ‘/var/www/index.html’:
ensure => file,
require => Package[‘nginx’],
}
service { ‘nginx’:
ensure => running,
enable = true,
}
}
This is the use of the let(:params)
section. Now, with that info, we are ready to expand on our rspec test.
init_spec.rb
:
require 'spec_helper'
describe 'nginx' do
let(:title) { 'nginx' }
let(:node) { 'test.example.com' }
it { is_expected.to compile } # this is the simplest test possible to make sure the Puppet code compiles
it { is_expected.to compile.with_all_deps } # same as above except it will test all the dependencies
#it { is_expected.to compile.and_raise_error(/error message/) # same again except it expects an error message
end
To test specific aspects of your code, like the package
resource, you would expand or replace the compile
part with something like this:
it { is_expected.to contain_package('nginx').with(ensure: ‘present’) } # each package would have a line like this.
To test the presence of the index.html
file in the catalog, the code would look something like this:
it { is_expected.to contain_file(‘/var/www/index.html')
.with(
:ensure => 'file',
:require => 'Package[nginx]',
)
}
Let's break this down just a bit. The first line, contain_file(‘/var/www/index.html’)
, is the title of the resource you are testing. The .with()
is a hash of the attributes of the resource to test. We can also continue expanding our test to include the service resource, and our finished code might look like this:
init_spec.rb
:
require 'spec_helper'
describe 'nginx' do
let(:title) { 'nginx' }
let(:node) { 'test.example.com' }
let(:facts) { {} } # Facts go here, and if no facts are needed, this can be omitted.
it { is_expected.to contain_package(‘nginx’).with(ensure: 'present') }
it { is_expected.to contain_file(‘/var/www/index.html')
.with(
:ensure => 'file',
:require => 'Package[nginx]',
)
}
it { is_expected.to contain_service(‘nginx')
.with(
:ensure => 'running',
:enabled => true,
)
}
end
To run your test, change to your module_name/spec
directory, and execute the following command:
rspec classes/init_spec.rb --format documentation
The command tests that specific file and tells you if your tests have passed or failed and where, so you can fix any issues that may come up.
You might want to ask, "Wait, what if I need to test more than just CentOS?" Ah, now you're talking. We can do that with contextual testing. Instead of testing just a single operating system, we can test others as well. We could use context
as in the example below, and write tests for each OS. However, there's a better way using rspec-puppet-facts
.
Using multiple contexts, init_spec.rb
:
require 'spec_helper'
describe 'nginx' do
let(:title) { 'nginx' }
let(:node) { 'test.example.com' }
context 'RedHat' do
let(:facts) { {
:osfamily => 'RedHat',
:operatingsystem => 'CentOS'
:architecture => 'x86_64',
} }
it { is_expected.to contain_package(‘nginx’).with(ensure: 'present') }
…
…
end
context 'Debian' do
let(:facts) { {
:osfamily => 'Debian',
:operatingsystem => 'Ubuntu
:architecture => 'x86_64',
} }
it { is_expected.to contain_package(‘nginx’).with(ensure: 'present') }
…
…
end
end
Using the rspec-puppet-facts
gem, the above code could then be simplified.
init_spec.rb
:
require 'spec_helper'
describe 'nginx' do
on_supported_os.each do |os, facts|
context "on #{os}" do
let(:facts) do
facts
end
it { is_expected.to contain_package(‘nginx’).with(ensure: 'present') }
it { is_expected.to contain_file(‘/var/www/index.html')
.with(
:ensure => 'file',
:require => 'Package[nginx]',
)
}
it { is_expected.to contain_service(‘nginx')
.with(
:ensure => 'running',
:enabled => true,
)
}
end
end
Congratulations. You have now completed your first Puppet class rspec-puppet test! You may ask, "How does this help me, though?" When changes are made to your class files — even minor changes — having a baseline set of tests that you can run against them will help determine whether you are properly maintaining your Puppet code. Couple that with puppet-lint
and puppet parser validate
and what you end up with is a solid base for producing working Puppet code that we know is tested and sound. Granted, there is a lot more to learn, and we covered just the basics with our example.
Now, a bit of extra credit, so to speak. What if your class has multiple packages to install or multiple directories to create? Do you really have to create a test for each one? No, you don't! You can take an array and the .each
iterator, and using syntax similar to that above, with just a little modification, you can test the catalog for the presence of the Apache, MySQL, phpMyAdmin, curl, and Wget packages.
Extra credit:
[‘apache’, ‘mysql’, ‘phpmyadmin’, ‘curl’, ‘wget’].each do |x| it {
is_expected.to contain_package(x)
.with(ensure: 'present')
}
end
Thanks to Carthik Sharma for asking me to take on learning rspec-puppet to write tests for our modules. Many thanks also to David Schmitt for helping me when I ran into problems, answering all of my beginner's questions, and showing me cool tricks like the .each
iterator.
Published at DZone with permission of Joseph Oaks. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments