No commit activity in last 3 years
No release in over 3 years
There's a lot of open issues
Helper classes for mock/stub functions, templates and hieradata
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme

rspec-puppet-utils

This is a more refined version of a previous project about rspec-puppet unit testing, it provides a class for mocking functions, a harness for testing templates, and a simple tool for testing hiera data files. The motivation for mocking functions etc is provided in that project so I won't go over it here.

See release notes about latest version

Updates:

v3.1.0

By default the zip file now includes the version (e.g. puppet-1.2.0.zip). In v3.0.0 the file was just called puppet.zip

You can omit the version from the zip name like so: puppet.package_versioning = false

Given that v3.0.0 was only released the day before, I'll keep it as a minor version update

v3.0.0

The project is now developed against ruby 2.1.0 and so it may not be backwards compatible when running on ruby 1.8.7.

The internal file structure has also changed, which shouldn't affect usage, but it might :)

v2.1.0

The MockFunction #stub and #expect methods have been superseded by #stubbed and #expected so that you can use parameter matchers. The only difference in usage from previous versions is that the methods take a set of parameters rather than a single array (e.g. f.expected.with(1, 2, 3) instead of f.expect.with([1, 2, 3]))

The change is backwards compatible so #stub and #expect are still available and function as before

Usage

MockFunction

The basic usage is to create your mock function with MockFunction.new and then use mocha to stub any particular calls that you need

require 'spec_helper'

describe 'foo::bar' do

  let!(:add_stuff) { MockFunction.new('add_stuff') { |f|
      f.stubbed.with(1, 2).returns(3)
    }
  }

  it 'should do something with add_stuff' do
    # Specific stub for this test
    add_stuff.stubbed.with(2, 3).returns(5)
    ...
  end
end

You can mock a function that doesn't return a value (:rvalue is the default):

MockFunction.new('func', {:type => :statement})

You can mock Hiera:

MockFunction.new('hiera') { |f|
  f.stubbed.with('non-ex').raises(Puppet::ParseError.new('Key not found'))
  f.stubbed.with('db-password').returns('password1')
}

You handle when the functions are created yourself, e.g. you can assign it to a local variable func = MockFunction... create it in a before block before(:each) do MockFunction... end or use let let!(:func) { MockFunction... }

If you use let, use let!() and not let(), this is because lets are lazy-loaded, so unless you explicitly reference your function in each test, the function won't be created and puppet won't find it. Using let! means that the function will be created before every test regardless.

Also if you use let when mocking hiera, you can't use :hiera as the name due to conflicts so you have to do something like let!(:mock_hiera) { MockFunction.new('hiera') }

Mocha stubs and expects:

f.stubbed and f.expected are helper methods for f.stubs(:execute) and f.expects(:execute)

Internally #expected will clear the rspec-puppet catalog cache. This is because rspec-puppet will only re-compile the catalog for a test if :title, :params, or :facts are changed. This means that if you setup an expectaion in a test, it might not be satisfied because the catalog was already compiled for a previous test, and so the functions weren't called!

Clearing the cache ensures tests aren't coupled and order dependent. The downside is that the catalog isn't cached and has to be re-compiled which slows down your tests. If you're concerned about performance and you are explicitly changing :title, :params, or :facts for a test, you can keep the cache intact with f.expected(:keep_cache)

Notes:
  • You always stub the execute method as that gets called internally
  • The execute method takes a set of arguments instead of an array of arguments

MockResource (experimental feature)

I've created a rough version for now just to help myself out, if people find it useful or find bugs, let me know

Usage:

To stop your tests dissapearing down a rabbit hole, you can use the rspec-puppet let(:pre_condition) { ... } feature to create mock versions of resources that your puppet class depends on. For example:

class my_module::my_class {

  include foo::bar
  
  $useful_var = $foo::bar::baz
  
  external_module::complex_type { 'complex thing':
    param_one      => 'one',
    param_two      => 'two',
    required_param => 'important value',
  }
  
  <actual stuff you want to test>
}

In the tests for my_class, you don't want to use the actual foo::bar and external_module::complex_type resources because it could be a lot of complex setup code, it can be difficult to test multiple scenarios, and you are by extension testing these other classes (which should have tests of their own)

You can therefore mock these resources by creating fakes that have the same "interface", but empty bodies:

let(:pre_condition) { [
    "class foo::bar { $baz = 'a value' }",
    "define external_module::complex_type ( $param_one = 'default', $param_two = undef, $required_param ) {}",
] }

This can get quite complex if there are multiple parameters and/or internal variables. MockResource is designed to make it easier to mock out these dependencies

mock_bar = MockResource.new 'foo::bar', {
    :vars => { :baz => some_var_you_want_to_use_later }
}

mock_complex_type = MockResource.new 'external_module::complex_type', {
    :type => :define,
    :params => {
        :param_one      => 'default',
        :param_two      => :undef,
        :required_param => nil,
    }
}

let(:pre_condition) { [
    mock_bar.render,
    mock_complex_thing.render,
] }

Hopefully you spend less time debugging syntax errors in your test strings, and more time writing useful code

TemplateHarness

If your templates have some logic in them that you want to test, you'd ideally like to get hold of the generated template so you can inspect it programmatically rather than just using a regex. In this case use TemplateHarness

Given a basic template:

<%
    from_class = @class_var
    from_fact  = scope.lookupvar('fact-name')
    from_hiera = scope.function_hiera('hiera-key')
-%>
<%= "#{from_class} #{from_fact} #{from_hiera}" %>

A test could look like this:

require 'spec_helper'

describe 'my_template' do

  let(:scope) { PuppetlabsSpec::PuppetInternals.scope }
  before(:each) do
    scope.stubs(:lookupvar).with('fact-name').returns('fact-value')
    scope.stubs(:function_hiera).with('hiera-key').returns('hiera-value')
  end
  
  it 'should render template' do
    harness = TemplateHarness.new('spec/.../.../my_template.erb', scope)
    harness.set('@class_var', 'classy')
    result = harness.run
    expect(result).to eq 'classy fact-value hiera-value'
  end

end

Note:

  • The path resolution is pretty simple, just pass it a normal relative path, not like the paths you pass into the template function in puppet (where you expect puppet to add the templates section to the path)

HieraData::Validator

The motivation behind this is to quickly check that your hiera data files have no syntax errors without having to run all of the possible combinations of your hiera hierarchy. At the moment this only supports yaml, but other file types can be added easily.

require 'spec_helper'

describe 'YAML hieradata' do

  # Files are loaded recursively
  validator = HieraData::YamlValidator.new('spec/fixtures/hieradata')
  validator.load_data :ignore_empty
  # Use load_data without args to catch empty files

  # Check types
  it 'should use arrays for api host lists' do
    validator.validate('my-api-hosts') { |v|
      expect(v).to be_an Array
    }
  end

  # Use regex to match keys
  it 'ports should only contain digits' do
    validator.validate(/-port$/) { |v|
      expect(v).to match /^[0-9]+$/
    }
  end

  # Supply a list of files that the key must be in
  # (all matches in all other files are still validated)
  # :live and :qa correspond to live.yaml and qa.yaml
  it 'should override password in live and qa' do
    validator.validate('password', [:live, :qa]) { |v|
      expect ...
    }
  end

end

In the examples above all keys in all yaml files are searched and checked

If there is an error, you'll see the inner RSpec error, as well as which key and which file is incorrect:

RSpecPuppetUtils::HieraData::ValidationError: mail-smtp-port is invalid in live: expected "TwoFive" to match /^[0-9]+$/
Diff:
@@ -1,2 +1,2 @@
-/^[0-9]+$/
+"TwoFive"

For more about usage see the wiki page

Setup

  • Add rspec-puppet-utils to your Gemfile (or use gem install rspec-puppet-utils)
  • Add require 'rspec-puppet-utils' to the top of your spec_helper

Rake Tasks (experimental feature)

rspec-puppet-utils also provides helper classes to add common rake tasks to a Puppet project or module.

Project Tasks

The Rake::Puppet class provides tasks that handle testing and building a Puppet project.

Usage

An example Rakefile might look like this:

require 'rspec_puppet_utils/rake/project_tasks'

puppet = Rake::Puppet.new
puppet.package_version = '1.0.0'
puppet.load_tasks

Running rake -T should now show a list of spec and build tasks:

$ rake -T
rake build            # Build puppet.zip v1.0.0
rake quick_build      # Build puppet.zip v1.0.0 without tests
rake spec             # Run specs in all modules
rake <mod a>:spec     # Run <mod a> module specs
rake <mod b>:spec     # Run <mod b> module specs
...

There is an spec task for each module, as well as a main spec task that will run all specs in a project.

The build task will bundle all Puppet code (modules, hiera data file, environment.conf files, etc) into a .zip file which can then be deployed.

In the example above package_version is set as it's a required field. The other accessible properties are:

  • module_path - The directory containing all the modules to test (default: 'modules')
  • excluded_modules - Modules to exclude from rspec testing (default: [])
  • package_dir - Where the puppet zip package will be created (default: 'pkg')
  • package_files - Files and directories to include in the package (default: ['modules', 'modules-lib', 'environment.conf'])
  • package_versioning - Is the version included in the package name? (default: true)
Setup

The spec task for each module actually just executes the spec task defined in the module's Rakefile, and the Rake::Puppet class will filter out modules that dont have a Rakefile,

Therefore each module needs the following two files:

# <mod>/Rakefile
require 'puppetlabs_spec_helper/rake_tasks'
# <mod>/spec/spec_helper.rb
require 'rspec-puppet-utils'
require 'puppetlabs_spec_helper/module_spec_helper'

Extra content/tasks/options/etc can be added to these files, but this is the suggested minimum

In order for the modules-lib modules to be available to the Puppet server, each environment will need an environment.conf file to set the module path, therefore an environment.conf file should be present in the project root:

# environment.conf
modulepath = ./modules:./modules-lib:$basemodulepath

Again, other settings can be changed, this is just the minimum to get the modules-lib pattern working

NB

The package_files list is setup for the modules-lib pattern by default. In this pattern external (e.g. Puppet Forge) modules are installed in a separate 'modules-lib', leaving the 'modules' dir for project modules such as 'components', 'profiles', 'role', etc. If you're not using this pattern then just provide a new array for package_files.

Running the build or quick_build tasks will delete any existing builds in the pkg directory. This is so the same build task can be run over and over on a build server (e.g. Jenkins) without filling up the disk. It also guarantees that the binary at the end of a build was just built, and wasn't left over from a previous build.

ToDo

Currently the spec task runs all the <module>:spec tasks. If one of these fails then none of the subsequent tasks will run. This isn't ideal!

The zip commands need to be replaced by ruby zip library to avoid shelling out, this helps with support for Windows environments