Stub library problem #562

Closed
maoueh opened this Issue Feb 11, 2015 · 14 comments

Projects

None yet

7 participants

@maoueh
Contributor
maoueh commented Feb 11, 2015

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

@sethvargo
Owner

You want this line:

allow(Chef::Recipe::StubProblem).to receive(:query).and_return("1.1")

And nothing else. What version of Chef?

@maoueh
Contributor
maoueh commented Feb 11, 2015

I tried with Chef 11.18 on Windows and Chef 12.03 on CentOS 6.5.

For the multiple lines, it's mainly to showcase what I tried. I also tried using a non-namespaced library file like this:

def query
 ...
end

And changing stubbing method accordingly without much luck.

@sethvargo
Owner

@maoueh can you use a non-Chef-namespaced thing?

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

The way you stub this is:

require_relative "../../libraries/helper"

describe "cookbook::recipe" do
  before do
    allow(MyHelper).to receive(:function)
  end

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

  it "does something" do
    expect(chef_run).to be
  end
end
@maoueh
Contributor
maoueh commented Feb 12, 2015

Using a module without using self works correctly:

# librarries/helper.rb
module MyHelper
  def function; end
end

But when using the self keyword, it's not working. I'm really not sure why. Tried to replicate using rspec alone with a similar test case (not identical I imagine however) but it works there.

A module without self is a good workaround in my case.

@maoueh
Contributor
maoueh commented Feb 12, 2015

Seems I was wrong as it works because the method is stubbed. Without self, the module is not usable as the original version is not available using MyHelper.query(). I was finding this strange the dropping self was working in fact.

I tried including the MyHelper by include directive does not seem to be available within recipes.

@maoueh
Contributor
maoueh commented Feb 12, 2015

I will try an existing cookbook using libraries and stubbing to work my way out of this. Will report with my findings.

@maoueh
Contributor
maoueh commented Feb 12, 2015

Ok, I followed how they do module mixin in https://github.com/opscode-cookbooks/homebrew cookbook and learnt how to include it in my recipes. I'm able to stub it this way.

# libraries/helper.rb
module Helper
  def query; end
end
# recipes/default.rb
Chef::Resource.send(:include, Helper)
Chef::Recipe.send(:include, Helper)

puts "Called in recipe directly: #{query()}"
# spec/recipes/default_spec.rb
...
  before do
    allow_any_instance_of(Chef::Resource).to receive(:query).and_return("1.1")
    allow_any_instance_of(Chef::Recipe).to receive(:query).and_return("1.1")
  end
...

That's a good compromise for now. I would prefer to namespace my call to library functions, but not big deal.

@sethvargo sethvargo closed this Feb 12, 2015
@sonots
sonots commented Feb 12, 2015

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)
@maoueh
Contributor
maoueh commented Feb 12, 2015

@sonots Wow thank you, exactly the explanation I wanted. I knew stub was overridden somewhere because it was working everywhere until really used, but I was just clueless where and why it was happening. Now, everything is clear.

Thanks again :D

@jamesmartin

@sonots, again, 👍, that's a really helpful answer. Something to this affect in the readme would probably help those familiar with RSpec, but not with Chef/Chefspec. We just spent two days trying to figure this out.

@joerg
joerg commented Jul 24, 2015

+1 for having this in the README. I also just spent about 2 hours figuring out what is going on until I found this thread.

@AaronKalair AaronKalair referenced this issue in jameslegg/chef-jlsolrcloud Feb 8, 2016
Merged

Various fixes #7

@marthinus-engelbrecht

@sonots you are my hero

@davidcpell

Also wrestled with this for quite a while today. Thanks @sonots.

@jamesmartin

PR for adding this workaround to the readme: #778.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment