Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Unit Testing With rspec-puppet for Beginners

DZone's Guide to

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.

· DevOps Zone
Free Resource

“Automated Testing: The Glue That Holds DevOps Together” to learn about the key role automated testing plays in a DevOps workflow, brought to you in partnership with Sauce Labs.

So, 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:

  1. /.fixtures.yml: Describes dependency modules.
  2. /Rakefile: Definition of Rake tasks.
  3. /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-lintand 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.

Learn about the importance of automated testing as part of a healthy DevOps practice, brought to you in partnership with Sauce Labs.

Topics:
unit testing ,puppet ,rspec ,devops

Published at DZone with permission of Joseph Oaks. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}