Not able to mock cookbook libraries objects #138

Closed
allinwonder opened this Issue May 16, 2013 · 15 comments

Comments

Projects
None yet
5 participants
@allinwonder

I am testing a recipe which is using a class I created in libraries folder. I want to stub a class method of it but failed. The real class is still being call after mocked. I don't know exactly why, my thinking maybe the issue happens related to module loading order. Please help

Please see the following code sample

File: /cookbooks/example/libraries/myclass.rb

class MyClass
    def self.run_me
        puts 'i am real method'
    end
end

File: /cookbooks/example/recipes/default.rb

MyClass.run_me

File: /cookbooks/example/spec/default_spec.rb

require '/cookbooks/example/libraries/myclass.rb'
require 'chefspec'
describe 'example::default' do
    context 'when myclass is mocked' do
        before {
           MyClass.stub(:run_me) do
               puts 'I am a mocked method'
           end
        }
        it 'should print I am a mocked method' do
          chef_run = ChefSpec::ChefRunner.new
          chef_run.converge 'example::default'
        end
    end
end

bundle exec rspec

i am real method
.

Finished in 0.01377 seconds
1 example, 0 failures
@sethvargo

This comment has been minimized.

Show comment
Hide comment
@sethvargo

sethvargo May 16, 2013

Collaborator

@allinwonder you need to require them in your spec_helper.rb as they are loaded to late in the Chef convergence phase: https://github.com/customink-webops/hostsfile/blob/master/spec/spec_helper.rb.

Collaborator

sethvargo commented May 16, 2013

@allinwonder you need to require them in your spec_helper.rb as they are loaded to late in the Chef convergence phase: https://github.com/customink-webops/hostsfile/blob/master/spec/spec_helper.rb.

@sethvargo sethvargo closed this May 16, 2013

@allinwonder

This comment has been minimized.

Show comment
Hide comment
@allinwonder

allinwonder May 16, 2013

Works, great! Heap of thanks

Update:
The provided solution works on instance methods. But not working for class methods.

The example above still prints out 'i am real method'

Works, great! Heap of thanks

Update:
The provided solution works on instance methods. But not working for class methods.

The example above still prints out 'i am real method'

@allinwonder

This comment has been minimized.

Show comment
Hide comment
@allinwonder

allinwonder May 28, 2013

Update:
The provided solution works on instance methods. But not working for class methods.

The example above is still red in test

default_spec.rb

require 'spec_helper'
describe 'example:default' do
    context 'when myclass is mocked' do
        before {
           MyClass.stub(:run_me) do
               puts 'I am a mocked method'
           end
        }
        it 'should print I am a mocked method' do
          chef_run = ChefSpec::ChefRunner.new
          chef_run.converge 'example::default'
        end
    end
end

spec_helper.rb

require 'chefspec'
require 'fauxhai'

lib = File.expand_path('../../libraries', __FILE__)
$:.unshift(lib) unless $:.include?(lib)

require 'myclass'

RSpec.configure do |c|
  c.filter_run
  c.run_all_when_everything_filtered = true
end

Update:
The provided solution works on instance methods. But not working for class methods.

The example above is still red in test

default_spec.rb

require 'spec_helper'
describe 'example:default' do
    context 'when myclass is mocked' do
        before {
           MyClass.stub(:run_me) do
               puts 'I am a mocked method'
           end
        }
        it 'should print I am a mocked method' do
          chef_run = ChefSpec::ChefRunner.new
          chef_run.converge 'example::default'
        end
    end
end

spec_helper.rb

require 'chefspec'
require 'fauxhai'

lib = File.expand_path('../../libraries', __FILE__)
$:.unshift(lib) unless $:.include?(lib)

require 'myclass'

RSpec.configure do |c|
  c.filter_run
  c.run_all_when_everything_filtered = true
end
@sethvargo

This comment has been minimized.

Show comment
Hide comment
@sethvargo

sethvargo May 28, 2013

Collaborator

You need to use rspec mocks as we discussed earlier.

Also have a look at .any_instance

Collaborator

sethvargo commented May 28, 2013

You need to use rspec mocks as we discussed earlier.

Also have a look at .any_instance

@allinwonder

This comment has been minimized.

Show comment
Hide comment
@allinwonder

allinwonder May 28, 2013

Hi Seth,

Thanks for your quick reply. I tried both to use
MyClass.should_receive(:run_me) and MyClass.stub(:run_me) to mock the class
method, neither is working.

Also I don't understand why I need to look into .any_instance for a class
method?

Thank you

Regards
Kevin

On Tue, May 28, 2013 at 12:02 PM, Seth Vargo notifications@github.comwrote:

You need to use rspec mocks as we discussed earlier.

Also have a look at .any_instance


Reply to this email directly or view it on GitHubhttps://github.com/acrmp/chefspec/issues/138#issuecomment-18521047
.

Hi Seth,

Thanks for your quick reply. I tried both to use
MyClass.should_receive(:run_me) and MyClass.stub(:run_me) to mock the class
method, neither is working.

Also I don't understand why I need to look into .any_instance for a class
method?

Thank you

Regards
Kevin

On Tue, May 28, 2013 at 12:02 PM, Seth Vargo notifications@github.comwrote:

You need to use rspec mocks as we discussed earlier.

Also have a look at .any_instance


Reply to this email directly or view it on GitHubhttps://github.com/acrmp/chefspec/issues/138#issuecomment-18521047
.

@sethvargo

This comment has been minimized.

Show comment
Hide comment
@sethvargo

sethvargo May 28, 2013

Collaborator

There's no way the provided solution will work on an instance method - it's only for class methods. You would need to use:

MyClass.any_instance.stub(:foo

to stub instance methods.

I'm able to reproduce your issue, and I don't think it's possible to test like this:

require 'spec_helper'

describe 'example:default' do
  context 'when myclass is mocked' do
    before do
      MyClass.stub(:run_me) { nil }
      MyClass.stub(:new) { klass }
    end

    let(:klass) { double('klass', run_me: nil) }

    it 'should not print anything' do
      chef_run = ChefSpec::ChefRunner.new(cookbook_path: $cookbook_paths)
      chef_run.converge('playground::default')
    end
  end
end

Because the chef run is going to reload that class, which is going to unmock the stubs. There's no way you can prevent that. Is there a strong reason you need to use class methods?

Collaborator

sethvargo commented May 28, 2013

There's no way the provided solution will work on an instance method - it's only for class methods. You would need to use:

MyClass.any_instance.stub(:foo

to stub instance methods.

I'm able to reproduce your issue, and I don't think it's possible to test like this:

require 'spec_helper'

describe 'example:default' do
  context 'when myclass is mocked' do
    before do
      MyClass.stub(:run_me) { nil }
      MyClass.stub(:new) { klass }
    end

    let(:klass) { double('klass', run_me: nil) }

    it 'should not print anything' do
      chef_run = ChefSpec::ChefRunner.new(cookbook_path: $cookbook_paths)
      chef_run.converge('playground::default')
    end
  end
end

Because the chef run is going to reload that class, which is going to unmock the stubs. There's no way you can prevent that. Is there a strong reason you need to use class methods?

@allinwonder

This comment has been minimized.

Show comment
Hide comment
@allinwonder

allinwonder May 28, 2013

Thanks Seth, and you proved my point that library class methods are not
being able to mock in chefspec.

In my project, I actually work around the issue by refactoring my class to
use instance methods.

I have no strong argument or use case to say class method is a single
solution. In worse case, class methods can be replaced by single instance
class. Maybe other contributors and users have better use cases on using
class methods.

And for better user experience, I think it will be good to add a note in
the README to describe this edge case, I have spent a couple of hours to
get to this point, and maybe a note in README can save users spending more
time on this problem.

If the user group or the authors think this is a future improvement, I can
open a feature request for this, you can put it into the backlog.

Thank you again for your helpful advices and response

Kev

Thanks Seth, and you proved my point that library class methods are not
being able to mock in chefspec.

In my project, I actually work around the issue by refactoring my class to
use instance methods.

I have no strong argument or use case to say class method is a single
solution. In worse case, class methods can be replaced by single instance
class. Maybe other contributors and users have better use cases on using
class methods.

And for better user experience, I think it will be good to add a note in
the README to describe this edge case, I have spent a couple of hours to
get to this point, and maybe a note in README can save users spending more
time on this problem.

If the user group or the authors think this is a future improvement, I can
open a feature request for this, you can put it into the backlog.

Thank you again for your helpful advices and response

Kev

@sethvargo

This comment has been minimized.

Show comment
Hide comment
@sethvargo

sethvargo May 28, 2013

Collaborator

Kevin,

I agree, I just want to clarify. This isn't a limit of chefspec, it's the way Chef is doing the converge by automatically requiring libraries.

This message was sent from my mobile device.

I apologize in advance for any typographical errors or autocorrections.

On May 27, 2013, at 8:48 PM, Kevin notifications@github.com wrote:

Thanks Seth, and you proved my point that library class methods are not
being able to mock in chefspec.

In my project, I actually work around the issue by refactoring my class to
use instance methods.

I have no strong argument or use case to say class method is a single
solution. In worse case, class methods can be replaced by single instance
class. Maybe other contributors and users have better use cases on using
class methods.

And for better user experience, I think it will be good to add a note in
the README to describe this edge case, I have spent a couple of hours to
get to this point, and maybe a note in README can save users spending more
time on this problem.

If the user group or the authors think this is a future improvement, I can
open a feature request for this, you can put it into the backlog.

Thank you again for your helpful advices and response

Kev

On Tue, May 28, 2013 at 1:26 PM, Seth Vargo notifications@github.comwrote:

There's no way the provided solution will work on an instance method -
it's only for class methods. You would need to use:

MyClass.any_instance.stub(:foo

to stub instance methods.

I'm able to reproduce your issue, and I don't think it's possible to test
like this:

require 'spec_helper'
describe 'example:default' do
context 'when myclass is mocked' do
before do
MyClass.stub(:run_me) { nil }
MyClass.stub(:new) { klass }
end

let(:klass) { double('klass', run_me: nil) }

it 'should not print anything' do
chef_run = ChefSpec::ChefRunner.new(cookbook_path: $cookbook_paths)
chef_run.converge('playground::default')
end
endend

Because the chef run is going to reload that class, which is going to
unmock the stubs. There's no way you can prevent that. Is there a strong
reason you need to use class methods?


Reply to this email directly or view it on GitHubhttps://github.com/acrmp/chefspec/issues/138#issuecomment-18523324
.


Reply to this email directly or view it on GitHub.

Collaborator

sethvargo commented May 28, 2013

Kevin,

I agree, I just want to clarify. This isn't a limit of chefspec, it's the way Chef is doing the converge by automatically requiring libraries.

This message was sent from my mobile device.

I apologize in advance for any typographical errors or autocorrections.

On May 27, 2013, at 8:48 PM, Kevin notifications@github.com wrote:

Thanks Seth, and you proved my point that library class methods are not
being able to mock in chefspec.

In my project, I actually work around the issue by refactoring my class to
use instance methods.

I have no strong argument or use case to say class method is a single
solution. In worse case, class methods can be replaced by single instance
class. Maybe other contributors and users have better use cases on using
class methods.

And for better user experience, I think it will be good to add a note in
the README to describe this edge case, I have spent a couple of hours to
get to this point, and maybe a note in README can save users spending more
time on this problem.

If the user group or the authors think this is a future improvement, I can
open a feature request for this, you can put it into the backlog.

Thank you again for your helpful advices and response

Kev

On Tue, May 28, 2013 at 1:26 PM, Seth Vargo notifications@github.comwrote:

There's no way the provided solution will work on an instance method -
it's only for class methods. You would need to use:

MyClass.any_instance.stub(:foo

to stub instance methods.

I'm able to reproduce your issue, and I don't think it's possible to test
like this:

require 'spec_helper'
describe 'example:default' do
context 'when myclass is mocked' do
before do
MyClass.stub(:run_me) { nil }
MyClass.stub(:new) { klass }
end

let(:klass) { double('klass', run_me: nil) }

it 'should not print anything' do
chef_run = ChefSpec::ChefRunner.new(cookbook_path: $cookbook_paths)
chef_run.converge('playground::default')
end
endend

Because the chef run is going to reload that class, which is going to
unmock the stubs. There's no way you can prevent that. Is there a strong
reason you need to use class methods?


Reply to this email directly or view it on GitHubhttps://github.com/acrmp/chefspec/issues/138#issuecomment-18523324
.


Reply to this email directly or view it on GitHub.

@dragonsmith

This comment has been minimized.

Show comment
Hide comment
@dragonsmith

dragonsmith Jul 14, 2014

Seth,
Please, can you explain how should I include libraries from dependency cookbooks if I'm using ChefSpec with berkshelf?

Seth,
Please, can you explain how should I include libraries from dependency cookbooks if I'm using ChefSpec with berkshelf?

@sethvargo

This comment has been minimized.

Show comment
Hide comment
@sethvargo

sethvargo Jul 14, 2014

Collaborator

They should be automatically required by Chef at runtime.

Collaborator

sethvargo commented Jul 14, 2014

They should be automatically required by Chef at runtime.

@dragonsmith

This comment has been minimized.

Show comment
Hide comment
@dragonsmith

dragonsmith Jul 14, 2014

But I want to mock a method from my custom library from a dependency cookbook. And specfile does not see my Module and cannot invoke it.

MyModule.stub(:my_method?) { true }

I get:

     NameError:
       uninitialized constant MyModule

But I want to mock a method from my custom library from a dependency cookbook. And specfile does not see my Module and cannot invoke it.

MyModule.stub(:my_method?) { true }

I get:

     NameError:
       uninitialized constant MyModule
@sethvargo

This comment has been minimized.

Show comment
Hide comment
@sethvargo

sethvargo Jul 14, 2014

Collaborator

You have already identified the problem (and solution) from your first phrase:

from a dependency cookbook

RSpec is an SUT framework, meaning you do not test things outside of your system. In cookbooks, the "system" is the current cookbook. Any dependencies (whether they are gems, other cookbooks, HTTP calls, etc) are to be mocked. You are trying to stub a method on the module, but you really need to stub the entire module:

let(:my_module) do
  double('MyModule', 
    my_method? : false
  )
end

before { stub_const('MyModule').and_return(my_module) }
Collaborator

sethvargo commented Jul 14, 2014

You have already identified the problem (and solution) from your first phrase:

from a dependency cookbook

RSpec is an SUT framework, meaning you do not test things outside of your system. In cookbooks, the "system" is the current cookbook. Any dependencies (whether they are gems, other cookbooks, HTTP calls, etc) are to be mocked. You are trying to stub a method on the module, but you really need to stub the entire module:

let(:my_module) do
  double('MyModule', 
    my_method? : false
  )
end

before { stub_const('MyModule').and_return(my_module) }
@ramarnat

This comment has been minimized.

Show comment
Hide comment
@ramarnat

ramarnat Jun 2, 2015

How could I stub included methods from a library in a provider?

libraries/foo.rb

module Foo
  def correct?
    # check something
  end
end

provider/foo.rb

include Foo

action :blah do
  if correct?
    # do something
  end
end

I have tried
allow_any_instance_of(Foo).to receive(:correct?).and_return(false)`` andallow(Full360::Configurator).to receive(:stage_task_running?).and_return(false)`

but it ignores the stub.

ramarnat commented Jun 2, 2015

How could I stub included methods from a library in a provider?

libraries/foo.rb

module Foo
  def correct?
    # check something
  end
end

provider/foo.rb

include Foo

action :blah do
  if correct?
    # do something
  end
end

I have tried
allow_any_instance_of(Foo).to receive(:correct?).and_return(false)`` andallow(Full360::Configurator).to receive(:stage_task_running?).and_return(false)`

but it ignores the stub.

@ramarnat

This comment has been minimized.

Show comment
Hide comment
@ramarnat

ramarnat Jun 2, 2015

ok found the solution - #562
(although I had to use allow_any_instance

ramarnat commented Jun 2, 2015

ok found the solution - #562
(although I had to use allow_any_instance

@paustin01

This comment has been minimized.

Show comment
Hide comment
@paustin01

paustin01 Dec 17, 2015

@ramarnat I'm running into your exact issue. Can you post what you did specifically?

@ramarnat I'm running into your exact issue. Can you post what you did specifically?

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