如何在 rspec 单元测试中隔离 Puppet 函数模拟
How to isolate Puppet function mocking in rspec unit tests
我有一个 Puppet class,它使用自定义 Puppet 函数的结果。为了确保我只测试 class 中的逻辑,而不是在对 class 进行单元测试时测试函数中的逻辑,我想模拟该函数。
但是,我似乎无法将我的模拟函数完全隔离到单个上下文中。我的真实测试代码比下面的例子大,但我已经把它归结为:
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)>'
我试着把它分成两个 describe
甚至两个单独的文件,结果总是一样的:一个上下文接收来自不同上下文的输出。
在我更大的测试用例中,大约有 20 个测试,它甚至更复杂,似乎受到某些上下文是否分配了事实的影响。上下文的顺序似乎并不重要。
我在这里错过了什么?
在撰写本文时(Puppet 6.6.0,Rspec-puppet 2.7.5),不幸的是,模拟 Puppet 函数的整个过程仍然有些混乱。 rspec-puppet 的 docs 仍然引用旧版 Ruby API 的功能,这无济于事。
正如 John Bollinger 在评论中所说,您面临的问题是,当加载 Rspec 文件时,您有一个编译器实例 运行s,然后断言it
稍后阻止 运行。
记住 Rspec(Rspec 本身,与 Puppet 无关)运行分两个阶段:
describe
和 context
块都在加载 Rspec 文件时计算。
it
个块,即示例本身,将被缓存并稍后评估。
Rspec 的作者在 Stack Overflow here 上对此有一个回答,我建议您看一看。
因此,为了避免为每个示例编译目录 - 这会使 Rspec-puppet 太慢 - 在执行 it
个示例之前缓存编译。
那你能做什么?
选项 1 - 使用 Tom Poulton 的 rspec-puppet-utils。
这具有 ready-made 解决方案的优势,该解决方案负责通过众所周知的接口模拟您的 Puppet 功能,并且使用 Tom 实现的 expected
功能,您还可以导致目录在不同的例子中重新编译。
缺点可能是它使用 Mocha 而不是 Rspec-mocks,它使用遗留的 Ruby API - 但是 Rspec-puppet 的文档也是如此! - 自 2017 年以来一直没有承诺。
因此你可以这样重写你的测试:
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
选项 2 - 窃取 Tom 的一些代码 - 猴子补丁 Rspec-puppet
然而,在幕后,Tom 的代码只是猴子补丁 Rspec-puppet,您可以窃取这样做的一点点并重构您的示例,如下所示:
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
选项 3 - 找到更好的方法
如果您在其他 Puppet 模块中搜索的时间足够长,您可能会找到更好的解决方案 - 甚至是使用 Puppet 4 函数的解决方案 API。也就是说,我想这对您的测试目的来说并不重要,只要假函数 returns 您期望的响应即可。
我有一个 Puppet class,它使用自定义 Puppet 函数的结果。为了确保我只测试 class 中的逻辑,而不是在对 class 进行单元测试时测试函数中的逻辑,我想模拟该函数。
但是,我似乎无法将我的模拟函数完全隔离到单个上下文中。我的真实测试代码比下面的例子大,但我已经把它归结为:
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)>'
我试着把它分成两个 describe
甚至两个单独的文件,结果总是一样的:一个上下文接收来自不同上下文的输出。
在我更大的测试用例中,大约有 20 个测试,它甚至更复杂,似乎受到某些上下文是否分配了事实的影响。上下文的顺序似乎并不重要。
我在这里错过了什么?
在撰写本文时(Puppet 6.6.0,Rspec-puppet 2.7.5),不幸的是,模拟 Puppet 函数的整个过程仍然有些混乱。 rspec-puppet 的 docs 仍然引用旧版 Ruby API 的功能,这无济于事。
正如 John Bollinger 在评论中所说,您面临的问题是,当加载 Rspec 文件时,您有一个编译器实例 运行s,然后断言it
稍后阻止 运行。
记住 Rspec(Rspec 本身,与 Puppet 无关)运行分两个阶段:
describe
和context
块都在加载 Rspec 文件时计算。it
个块,即示例本身,将被缓存并稍后评估。
Rspec 的作者在 Stack Overflow here 上对此有一个回答,我建议您看一看。
因此,为了避免为每个示例编译目录 - 这会使 Rspec-puppet 太慢 - 在执行 it
个示例之前缓存编译。
那你能做什么?
选项 1 - 使用 Tom Poulton 的 rspec-puppet-utils。
这具有 ready-made 解决方案的优势,该解决方案负责通过众所周知的接口模拟您的 Puppet 功能,并且使用 Tom 实现的 expected
功能,您还可以导致目录在不同的例子中重新编译。
缺点可能是它使用 Mocha 而不是 Rspec-mocks,它使用遗留的 Ruby API - 但是 Rspec-puppet 的文档也是如此! - 自 2017 年以来一直没有承诺。
因此你可以这样重写你的测试:
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
选项 2 - 窃取 Tom 的一些代码 - 猴子补丁 Rspec-puppet
然而,在幕后,Tom 的代码只是猴子补丁 Rspec-puppet,您可以窃取这样做的一点点并重构您的示例,如下所示:
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
选项 3 - 找到更好的方法
如果您在其他 Puppet 模块中搜索的时间足够长,您可能会找到更好的解决方案 - 甚至是使用 Puppet 4 函数的解决方案 API。也就是说,我想这对您的测试目的来说并不重要,只要假函数 returns 您期望的响应即可。