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 describe
s 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?
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:
describe
and context
blocks are all evaluated at the time the Rspec files are loaded.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?
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
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With