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 for v2.0.1

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.stub.with([1, 2]).returns(3)
    }
  }

  it 'should do something with add_stuff' do
    # Specific stub for this test
    add_stuff.stub.with([]).returns(nil)
    ...
    ...
  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.stub.with(['non-ex']).raises(Puppet::ParseError.new('Key not found'))
  f.stub.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.stub and f.expect are helper methods for f.stubs(:call) and f.expects(:call)

Internally #expect 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.expect(:keep_cache)

Notes:
  • You always stub the call method as that gets called internally
  • The call method takes an array of arguments

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