DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Unit Testing With rspec-puppet for Beginners

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.

Joseph Oaks user avatar by
Joseph Oaks
·
Jan. 26, 17 · Tutorial
Like (0)
Save
Tweet
Share
7.55K Views

Join the DZone community and get the full member experience.

Join For Free

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.

unit test

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

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Hidden Classes in Java 15
  • SAST: How Code Analysis Tools Look for Security Flaws
  • What Is Policy-as-Code? An Introduction to Open Policy Agent
  • Mr. Over, the Engineer [Comic]

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: