0

I have created a very simple custom resource in Chef, and inside that resource is some simple logic. The logic in question calls out to some custom helper methods.

I can build the resource, and execute it as part of a recipe - but if I would like to unit test the behaviour within the resource itself to ensure the flow is correct. As such, I want to be able to mock the behaviour of those helper functions so that I can direct the resource behaviour. Unfortunately, I can't get this to work.

My recipe looks like this:

my_resource 'executing' do
    action :execute
end

The resource looks like this:

action :execute do
  if my_helper?(node['should_be_true'])
    converge_by "Updating" do
      my_helper_method
    end
  end
end

action_class do
  include CustomResource::Helpers
end

The functions are simple:

module CustomResource
  module Helpers
    def my_helper?(should_be_true)
      should_be_true
    end

    def my_helper_method
      hidden_method
    end

    def hidden_method
      3
    end
  end
end

When I try to mock the behaviour of these in my ChefSpec tests, I get errors:

it 'executes' do
  allow(CustomResource::Helpers).to receive(:my_helper?).and_return(true)
  expect(CustomResource::Helpers).to receive(:my_helper_method)
  expect { chef_run }.to_not raise_error
end


Failure/Error: expect(CustomResource::Helpers).to receive(:my_helper_method)

   (CustomResource::Helpers).my_helper_method(*(any args))
       expected: 1 time with any arguments
       received: 0 times with any arguments

Any ideas what I'm doing wrong in my mocking?

Thanks in advance!

bummi
  • 27,123
  • 14
  • 62
  • 101
hairydave
  • 11
  • 3
  • I do not think you are calling `CustomResource::Helpers.my_helper?`. After include you are calling just a `::my_helper?`, I am not sure what is root class for the resource. Try calling method directly with the module name. In addition you may need to add `unless defined?(CustomResource::Helpers) do .. end` at the top of your helper file, I have seen issues with libraries files loading order. – Szymon Oct 17 '17 at 04:33
  • Thanks... I think I understand the problem now. I've tried calling the method directly before, but that doesn't work when you converge the recipe (weird). Instead, including the module into the action_class means that the module is included into the Chef::Resource::ActionClass instance that is associated with my resource - and so I have to mock a different type of class. I will post my - possibly incorrect, but working - solution shortly. – hairydave Oct 17 '17 at 08:45
  • Hi hairydave, please do not add solutions to the question, add an own answer and accept it. – bummi Oct 21 '17 at 11:28
  • Sure, but you could at least have left it there for me to move... – hairydave Oct 21 '17 at 12:20

2 Answers2

1

Managed to make this work by changing the mocking method... Those module functions are added into the action_class, and therefore at runtime they are methods on that specific instance of the resource's ActionClass. Not sure if my solution is right/ideal - but it does work:

include CustomResource::Helpers

<snip>

it 'executes' do
  allow_any_instance_of(Chef::Resource::ActionClass).to receive(:my_helper?).and_return(true)
  expect_any_instance_of(Chef::Resource::ActionClass).to receive(:my_helper_method)
  expect { chef_run }.to_not raise_error
end

I did look into avoiding the 'any instance of' mock, but then I got into problems and gave up - clearly the ActionClass has a lot of behaviour that I don't want to have to worry about.

hairydave
  • 11
  • 3
0

This is unfortunately very difficult given how ChefSpec works. You need to stub it on any instance of your resource, because that's the actual receiver. If you can use a module method instead, that is slightly easier. I take it you're already require_relative-ing your library file or the code would fail differently. However there is no way to do that for a custom resource because they are DSL-y and Ruby can't load them directly. The simplest option is generally to not test this kind of thing. Put code in the helper methods that checks if a node attribute is set and use that value if it is (specifics depend on the exact case), and then set that in your test. It's gross and annoying though, which is part of why Halite exists.

coderanger
  • 52,400
  • 4
  • 52
  • 75