4

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

k6ps
  • 359
  • 4
  • 11

3 Answers3

3

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.

Tensibai
  • 15,557
  • 1
  • 37
  • 57
  • Just thought that there must be an easy way to add a dummy service to `chef_run` , just my Ruby skills are poor. I tried to create a `double` and add it to `resource_collection` of `chef_run` but that did not work for reasons i did not understand. So i finally allowed `another_recipe` to run. This turned out not as bad as i expected, and this helped me in other unit tests i wrote later. So thanks for ideas :) – k6ps Mar 16 '15 at 10:01
  • @k6ps Well, see the end of my answer, if you have only one service to mock you may create it in your recipe in a `if defined?(ChefSpec)` block which will be evaluated only in the context of chefspec (so no harm in normal run) – Tensibai Mar 16 '15 at 11:06
1
describe 'mycookbook::default' do
  context 'something' do
    let(:solo) do
      ChefSpec::SoloRunner.new
    end 

    let(:chef_run) do
      solo.converge(described_recipe) do
        solo.resource_collection.insert(
                Chef::Resource::Service.new('apache2', solo.run_context))
      end 
    end 

    it 'runs without errors' do
      allow_any_instance_of(Chef::Recipe).to receive(:include_recipe)
      chef_run
    end 
  end 
end
Marcel Tricolici
  • 442
  • 3
  • 14
0

Another way would be to include a begin/rescue in your recipe so the service is added to the resource collection if necessary. The default action is :nothing so you don't need anything more than to list it in the rescue block.

begin
  svc = resources('service[my-service]')
rescue
  svc = service 'my-service'
end

template "my_dir/my_file.txt" do
  source "my_file.txt.erb"
  owner 'myuser'
  group 'mygroup'
  notifies :restart, svc, :delayed
end