New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stub library problem #562

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

Comments

Projects
None yet
8 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

This comment has been minimized.

Show comment
Hide comment
@sethvargo

sethvargo Feb 11, 2015

Collaborator

You want this line:

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

And nothing else. What version of Chef?

Collaborator

sethvargo commented Feb 11, 2015

You want this line:

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

And nothing else. What version of Chef?

@maoueh

This comment has been minimized.

Show comment
Hide comment
@maoueh

maoueh Feb 11, 2015

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@sethvargo

sethvargo Feb 11, 2015

Collaborator

@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
Collaborator

sethvargo commented Feb 11, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@maoueh

maoueh Feb 12, 2015

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@maoueh

maoueh Feb 12, 2015

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@maoueh

maoueh Feb 12, 2015

Contributor

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

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

This comment has been minimized.

Show comment
Hide comment
@maoueh

maoueh Feb 12, 2015

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@sonots

sonots 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)

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

This comment has been minimized.

Show comment
Hide comment
@maoueh

maoueh Feb 12, 2015

Contributor

@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

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

This comment has been minimized.

Show comment
Hide comment
@jamesmartin

jamesmartin Apr 23, 2015

@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.

jamesmartin commented Apr 23, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@joerg

joerg 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.

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 Feb 8, 2016

Merged

Various fixes #7

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Oct 1, 2016

@sonots you are my hero

ghost commented Oct 1, 2016

@sonots you are my hero

@davidcpell

This comment has been minimized.

Show comment
Hide comment
@davidcpell

davidcpell Nov 10, 2016

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

davidcpell commented Nov 10, 2016

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

@jamesmartin

This comment has been minimized.

Show comment
Hide comment
@jamesmartin

jamesmartin Nov 11, 2016

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

jamesmartin commented Nov 11, 2016

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

@miroswan

This comment has been minimized.

Show comment
Hide comment
@miroswan

miroswan May 11, 2017

Contributor

Does this work for cookbooks that don't use a class directly in the recipe? I have a class that calls a method from another class and when i use allow_instance_of(OTHER_CLASS).to receive(:my_method) it doesn't stub.

Contributor

miroswan commented May 11, 2017

Does this work for cookbooks that don't use a class directly in the recipe? I have a class that calls a method from another class and when i use allow_instance_of(OTHER_CLASS).to receive(:my_method) it doesn't stub.

@miroswan

This comment has been minimized.

Show comment
Hide comment
@miroswan

miroswan May 12, 2017

Contributor

Ah, when I pulled the class from from the Chef::Recipe namespace and just used the class as the namespace it worked.

Contributor

miroswan commented May 12, 2017

Ah, when I pulled the class from from the Chef::Recipe namespace and just used the class as the namespace it worked.

@lamont-granquist

This comment has been minimized.

Show comment
Hide comment
@lamont-granquist

lamont-granquist Jul 5, 2018

Contributor

the preload! method should be usable now to make this better, see #903 and the huge rewrite in #905

Contributor

lamont-granquist commented Jul 5, 2018

the preload! method should be usable now to make this better, see #903 and the huge rewrite in #905

@chefspec chefspec locked and limited conversation to collaborators Jul 5, 2018

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