Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to isolate Puppet function mocking in rspec unit tests

I have a Puppet class that uses the result of a custom Puppet function. To make sure I only test the logic in my class, and not the logic in my function when doing unit tests for the class, I want to mock the function.

However, I can't seem to fully isolate my mocked function to a single context. My real testing code is bigger than the following example, but I've boiled it down to this:

class break_tests {

  $result = my_mocked_function('foo', 'bar', 'baz')

  file { 'under_test':
    content => $result,
  }

}
require 'spec_helper'

def mock_mmf(return_value)
  Puppet::Parser::Functions.newfunction(:'my_mocked_function', type: :rvalue) do |_args|
    return return_value
  end
end

# rubocop:disable Metrics/BlockLength
describe 'break_tests' do
  context 'numero uno' do
    before { mock_mmf('foo') }
    it { should contain_file('under_test').with_content('foo') }
  end
  context 'numero duo' do
    before { mock_mmf('bar') }
    it { should contain_file('under_test').with_content('bar') }
  end
end
Failures:

  1) break_tests numero duo should contain File[under_test] with content  supplied string
     Failure/Error: it { should contain_file('under_test').with_content('bar') }
       expected that the catalogue would contain File[under_test] with content set to supplied string
     # ./spec/classes/break_tests_spec.rb:17:in `block (3 levels) in <top (required)>'

I tried splitting it up into two describes and even two separate files, the result is always the same: one context receives the output from a different context.

In my bigger test case, with about 20 tests, it's even more complex, seemingly influenced by whether or not some contexts have facts assigned to them. Ordering of the contexts does not seem to matter.

What am I missing here?

like image 202
Simon Avatar asked Sep 11 '25 13:09

Simon


1 Answers

At the time of writing (Puppet 6.6.0, Rspec-puppet 2.7.5), the whole business of mocking Puppet functions remains all a bit of a mess unfortunately. It doesn't help that rspec-puppet's docs still refer to the legacy Ruby API for functions.

The problem that you're facing is as John Bollinger has said in the comments, that you have a compiler instance that runs when the Rspec files are loaded, and then assertions in it blocks that run later.

Remember that Rspec (Rspec itself, nothing to do with Puppet) runs in two phases:

  1. The describe and context blocks are all evaluated at the time the Rspec files are loaded.
  2. The it blocks, the examples themselves, are cached and evaluated later.

There is an answer on this by Rspec's author at Stack Overflow here that I recommend having a look at.

So, to avoid the catalog being compiled for every single example - which would make Rspec-puppet way too slow - the compilation is cached prior to the it examples being executed.

So what can you do?

Option 1 - Use Tom Poulton's rspec-puppet-utils.

This has the advantage of a ready-made solution that takes care of mocking your Puppet functions through a well known interface, and using the expected feature Tom has implemented, you can also cause the catalogs to be recompiled in different examples.

The disadvantages could be that it uses Mocha rather than Rspec-mocks, it uses the legacy Ruby API - but then so do Rspec-puppet's docs! - and it hasn't been committed to since 2017.

Thus you could rewrite your tests this way:

require 'spec_helper'
require 'rspec-puppet-utils'

def mock_mmf(return_value)
  MockFunction.new('my_mocked_function').expected.returns(return_value)
end

describe 'test' do
  context 'numero uno' do
    before { mock_mmf('foo') }
    it { should contain_file('under_test').with_content('foo') }
  end
  context 'numero duo' do
    before { mock_mmf('bar') }
    it { should contain_file('under_test').with_content('bar') }
  end
end

Option 2 - Steal some of Tom's code - monkey patch Rspec-puppet

Under the hood however, Tom's code just monkey patches Rspec-puppet, and you could just steal the little bit that does that and refactor your examples like this:

require 'spec_helper'
require 'rspec-puppet/cache'

module RSpec::Puppet  ## Add this block
  module Support
    def self.clear_cache
      @@cache = RSpec::Puppet::Cache.new
    end
  end
end

def mock_mmf(return_value)
  RSpec::Puppet::Support.clear_cache  ## ... and this line
  Puppet::Parser::Functions.newfunction(:'my_mocked_function', type: :rvalue) do |_args|
    return return_value
  end
end

describe 'test' do
  context 'numero uno' do
    before { mock_mmf('foo') }
    it { should contain_file('under_test').with_content('foo') }
  end
  context 'numero duo' do
    before { mock_mmf('bar') }
    it { should contain_file('under_test').with_content('bar') }
  end
end

Option 3 - find a better way

If you search around in other Puppet modules for long enough, you may find better solution - even solutions that use the Puppet 4 Function API. That said, I guess it doesn't matter so much for the purpose of your test, as long as the fake function returns the response you expect.

like image 95
Alex Harvey Avatar answered Sep 14 '25 02:09

Alex Harvey