Puppet module rspec testing with PDK

September 5, 2019 

If you've searched for instructions on how to write unit tests for Puppet code you've probably stumbled upon tutorials such as this that are of good quality, but suggest installing and using tools such as puppetlabs_rspec_helper directly. Or you may encounter references to rake, bundler, rvm and rspec. Then you start experimenting these tools, some of which (rvm and bundler) are aimed at managing the dependency hell that arises when you, or your team, works simultaneously on multiple projects with different dependency requirements. In the end you are maybe able to run spec tests on your Puppet module, but after all installations of gems and experiments you're unsure what you did to make everything work. You also don't want to become a hard-core Ruby developer who actually understands all these tools well - you just want to test your Puppet code.

Fortunately The Puppet Development Kit, PDK in short, greatly simplifies the process of setting up the plumbing required to run (and write) rspec tests. Once you've installed PDK you need to make it PDK-compatible with "pdk convert" which adds lots of files to your module required by the underlying "raw" utilities (e.g. bundler, rspec) to work correctly for Puppet module testing.

To add a unit test for an existing class in a module, here "sshd", use

$ pdk new test -u sshd
 pdk (INFO): Using Ruby 2.4.5
 pdk (INFO): Using Puppet 5.5.16
 [✔] Examining module contents
 pdk (INFO): Creating 'puppet-sshd/spec/classes/sshd_spec.rb' from template.

The PDK-generated spec test was quite simplistic:

require 'spec_helper'

describe 'sshd' do
on_supported_os.each do |os, os_facts|
context "on #{os}" do
let(:facts) { os_facts }
it { is_expected.to compile }

The syntax of this file is described in rspec-puppet-facts documentation, but basically this code loops through the supported operating systems in the module's metadata.json file and tries to compile the Puppet catalog for supported operating system it finds. The facts come from facterdb which is a catalogue of facts you can use in spec testing, instead of defining them manually for each operating system and architecture combination you want to test again.

Now, in the case of the module tested above ("puppet-sshd") the above code was not sufficient for several reasons:

  1. Missing dependency modules
  2. Missing custom facts
  3. Undefined standard facts
  4. Missing required class parameters

Missing dependency modules are automatically fetched, but only if they are defined in a file called .fixtures.yml at the root of the Puppet module. In this case it contains the following dependencies:

repo: "puppetlabs/firewall"
ref: "2.0.0"
repo: "puppetfinland/monit"
ref: "1.2.1"
repo: "puppetfinland/os"
ref: "1.1.3"
repo: "puppetfinland/packetfilter"
ref: "2.0.3"
repo: "puppetlabs/stdlib"
ref: "4.25.1"
repo: "puppetfinland/systemd"
ref: "1.0.0"

These are all modules from Puppet Forge, but puppetlabs_spec_helper documentation describes the syntax of this file in detail.

Fixing missing custom facts and undefined built-in facts (e.g. $::lsbdistcodename on RedHat) requires modifying the spec file, here spec/classes/sshd_spec.rb:

require 'spec_helper'

describe 'sshd' do
on_supported_os.each do |os, os_facts|
context "on #{os}" do
case os_facts[:osfamily]
when 'RedHat'
# RedHat operating systems don't have the $::lsbdistcodename fact
extra_facts = { :lsbdistcodename => 'RedHat', :has_systemd => true }
# The custom $::has_systemd fact is not available in spec testing,
# so we give it a static value which is good enough for testing purposes
extra_facts = { :has_systemd => true }
# This combines our extra facts with the standard facts that come from facterdb
let(:facts) do

# Pass a mandatory parameter to the class
let(:params) { { :monitor_email => 'root@localhost' } }

# We just want to know that the class compiles
it { is_expected.to compile } end

Now the spec tests actually succeed:

$ pdk test unit
pdk (INFO): Using Ruby 2.5.1
pdk (INFO): Using Puppet 5.5.16
[✔] Preparing to run the unit tests.
[✔] Running unit tests.
Run options: exclude {:bolt=>true}
Evaluated 8 tests in 10.284742983 seconds: 0 failures, 0 pending.

To limit testing to one or more test files go to the module root and define the test file(s) pdk should process:

$ pdk test unit --tests=spec/classes/sshd_spec.rb

See official PDK documentation for details.

More on Puppet testing:

Samuli Seppänen
Samuli Seppänen
Author archive