Search code examples
unit-testingrspecchef-infrachef-recipechefspec

How to create a dummy resource defined in another recipe without including the other recipe in test run?


I have the following Chef recipe:

# recipes/default.rb

include_recipe 'my-cookbook::another_recipe'

execute 'Do some stuff' do
  command "echo some stuff"
  action :run
end

template "my_dir/my_file.txt" do
  source "my_file.txt.erb"
  owner 'myuser'
  group 'mygroup'
  notifies :restart, resources(:service => 'my-service'), :delayed
end

and another recipe

# recipes/another_recipe.rb

service 'my-service' do
  start_command "my-service start"
  stop_command "my-service stop"
  supports :start => true, :stop => true
  action :nothing
end

Now i want to write a Chefspec unit test to the default recipe in isolation. So i wrote this:

# spec/unit/recipes/default_spec.rb

require 'rspec/core'
require 'chefspec'

describe 'my-cookbook::default' do
  let(:chef_run) { ChefSpec::SoloRunner.converge(described_recipe) }

  before do
    allow_any_instance_of(Chef::Recipe).to receive(:include_recipe).with('my-cookbook::another_recipe')
  end

  it "Does the stuff" do
    expect(chef_run).to run_execute("echo some stuff")
  end

end

How do i create a dummy of the service defined in another_recipe to prevent this happening:

 11:    group 'mygroup'
 12>>   notifies :restart, resources(:service => 'my-service'), :delayed
 13:  end
...
Failure/Error: let(:chef_run) { ChefSpec::SoloRunner.converge(described_recipe) }
Chef::Exceptions::ResourceNotFound:
Cannot find a resource matching service[my-service] (did you define it first?)

I know that this is probably a bad design and a fairly simple newbie question, but i'm really stuck here and my situation is this:

  • i have a few months of experience with Chef, but no Ruby experience beyond that
  • this is a production code that was originally written without any unit tests
  • the real code contains much more stuff, these code snippets here are just a model of the particular problem
  • i'm given a task to modify the default recipe, so i thought to add a few unit tests to verify that my modifications work and do not break existing functionality
  • at this point i want to avoid modifying any other files (i.e. the another_recipe) as much as possible
  • if i let the test run also the another_recipe then i'd need to mock and set too many other things it needs

Thanks :) k6ps


Solution

  • My point of view:

    You should allow the another_recipe to be in run and do the necessary for it to converge. If not you can't really trust your test as they're not done against what will happen in a run.

    Workaround to answer your case anyway:

    Well you may add a "mockers recipe" in your cookbook which will define no-ops resources you need to test your recipe without too much stub/mock calls.

    Lets say it's called 'spec-receipe.rb' and it looks like:

    service 'my-service' do
      action :nothing
    end
    

    Then you run your tests including this 'spec-recipe' like this:

    let(:chef_run) { ChefSpec::SoloRunner.converge('my_cookbook::spec-recipe',described_recipe) }
    

    Another way could be to include_recipe 'my_cookbook::spec-recipe' if defined?(ChefSpec) so this recipe would be included only in a chefspec run and not in normal run and you don't have to specify it in the runner declaration.