chefspec: Stub library problem

I’m unsure if it’s a bug or a bad usage on my part. I’m trying to stub a cookbook library class method. I read all old closed issues about stubbing libraries and implemented ideas from lots of them, but I’m just unable to make it work correctly.

The stub is correct when called in the before block, in the runner new block and in the example directly. But when called within the recipe file default.rb, the original method is called. I really don’t understand why it’s not working.

I’m willing to debug this, so any insights or ideas is welcome.

You can check a minimal test case here: https://github.com/maoueh/chefspec-stub-problem

Regards, Matt

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 17 (9 by maintainers)

Most upvoted comments

Let me write a note here

CAUSE

When ChefSpec::SoloRunner.converge is called, Chef::Client#setup_run_context will be called at

From: /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chefspec-4.2.0/lib/chefspec/solo_runner.rb @ line 106 ChefSpec::SoloRunner#converge:

    105: def converge(*recipe_names)
    106:   node.run_list.reset!
    107:   recipe_names.each { |recipe_name| node.run_list.add(recipe_name) }
    108:
    109:   return self if dry_run?
    110:
    111:   # Expand the run_list
    112:   expand_run_list!
    113:
    114:   # Setup the run_context
 => 115:   @run_context = client.setup_run_context
    116:
    117:   # Allow stubbing/mocking after the cookbook has been compiled but before the converge
    118:   yield if block_given?
    119:
    120:   @converging = true    121:   @client.converge(@run_context)
    122:   self
    123: end

and, blah blah, it will finally Kernel.load libraries at Chef::RunContext::CookbookCompiler#load_libraries_from_cookbook

# lib/chef/run_context/cookbook_compiler.rb
187       def load_libraries_from_cookbook(cookbook_name)
188         files_in_cookbook_by_segment(cookbook_name, :libraries).each do |filename|
189           begin
190             Chef::Log.debug("Loading cookbook #{cookbook_name}'s library file: #{filename}")
191             Kernel.load(filename)
192             @events.library_file_loaded(filename)
193           rescue Exception => e
194             @events.library_file_load_failed(filename, e)
195             raise
196           end
197         end
198       end

Notice that it is load, not require.

So, when you write libraries and spec:

# libraries/helper.rb
module MyHelper
  def self.function; end
end
require_relative "../../libraries/helper"

describe "cookbook::recipe" do
  before do
    allow(MyHelper).to receive(:function)
    # Here, you are stubbing MyHelper.function, but!!!
  end

  let(:chef_run) { ChefSpec::SoloRunner.converge(described_recipe) }
  # In side convergence, libraries are loaded again, and the stubbing is overwritten by real implemntation!

  it "does something" do
    expect(chef_run).to be
  end
end

Here, you are stubbing MyHelper.function, but

  before do
    allow(MyHelper).to receive(:function)
  end

Inside converge, libraries are loaded again, and the stubbing is overwritten by real implemntation

  let(:chef_run) { ChefSpec::SoloRunner.converge(described_recipe) }

HOW TO RESOLVE

Write libraries as following not to be loaded again:

# libraries/helper.rb
module MyHelper
  def self.function; end
end unless defined?(MyHelper)