ChefSpec - how to test the Chef libraries referenced by other recipes?

I am just wondering what would be the best practice to unit test Chef recipes and cookbooks? I have a couple of Chef libraries referenced by some Chef recipes, should I write separate ChefSpec for those libraries or I can unit test the libraries inside the ChefSpec test for the recipes referencing them?

Chef 
|__cookbooks 
|     |_myCookbook 
|        |__libraries 
|        |    |__get_full_repository_name.rb 
|        |__recipes 
|        |     |__default.rb
|        |__spec
|             |__defaut_spec.rb
|        |__metadata.rb
|__ ... ...

Since the default recipe references get_full_repository_name library to return the name, in the ChefSpec test for the default recipe, how could I mock the result of get_full_repository_name (maybe just connection response) to test the recipe as if I don't mock the data, I believe the test will continue going into the library which I dont think it's a right way to test the recipe and also raised a question whether should I test the library as part of testing recipe or separate these into 2 tests.

Thanks.

It would depend on how complex your library is. Certainly the simplest way is to mock different return values for your different functions:

  before do
    allow_any_instance_of(Chef::Recipe)
      .to receive(:my_function_name)
      .with("this argument") # optional only if your function takes an arg
      .and_return("return value")
  end

Add a context with a different before block for each exemplar. This doesn't test the function but it tests what your recipe will do when it receives different return values from the function. These tests will not step into your actual function and are useful if the function just wraps an external call or something.

If your library is more complex and has several functions packaged in a module, it is best to create a different spec file and test the module like a normal ruby module, using vanilla rspec.

# spec/get_full_repository_name_spec.rb
require_relative '../libraries/get_full_repository_name.rb'

describe MyOrg::GetFullRepositoryName do
  let(:subject) { Class.new { extend MyOrg::GetFullRepositoryName } }

  # do tests
end

Make sure your library is namespaced either with Class or Module so you can include it somehow:

# libraries/get_full_repository_name.rb
module MyOrg
  module GetFullRepositoryName
    def get_repo_name
      "return value"
    end
  end
end
Chef::Recipe.send(:include, MyOrg::GetFullRepositoryName)
1 Like

This is the solution that I am looking for. However, how could I test the return value in ChefSpec if the return value is still the last variable of the function?

# libraries/get_full_repository_name.rb
module MyOrg
  module GetFullRepositoryName
    def get_repo_name(source_name)
      source = "#{node['source_path']}/#{source_name}"
      full_repo_name = "#{source}_repository"
    end
  end
end

I tried this, but it's not working

# spec/get_full_repository_name_spec.rb
require_relative '../libraries/get_full_repository_name.rb'

describe MyOrg::GetFullRepositoryName do
  let(:subject) { Class.new { extend MyOrg::GetFullRepositoryName } }

  let(:chef_run) do
        ChefSpec::SoloRunner.new(platform: 'windows', version: '2016') do |node|
            node.default['source_path'] = "example"
        end.converge(described_recipe)
    end

    it 'has valid name' do
      expect(MyOrg::GetFullRepositoryName.get_full_repository_name("chef_test")).to eq("example/chef_test_repository")
    end

end

And another question is that I got another library that connects to Azure Key vault to retrieve some passwords via using Chef::EncryptedDataBagItem.load to load the access key. What is the best way you think that could mock and test this out in ChefSpec test for this library?

#../library/get_password.rb
def get_password(base_username)

            service_name = "#{[node['server_type']}_#{base_username}"
    
            key_vault = Chef::EncryptedDataBagItem.load("key_vault", node['key_vault_data_bag'])
        
            key_vault_service_principal = {
                'tenant_id' => key_vault["tenant_id"],
                'client_id' => key_vault["client_id"],
                'secret' => key_vault["key_vault_secret"]
            }
        
            service_account_password = akv_get_secret(key_vault["key_vault_name"],
                service_name.gsub("_","-").downcase,
                key_vault_service_principal)
        end

Thanks so much.

Something like:

require_relative '../libraries/get_full_repository_name.rb'

describe MyOrg::GetFullRepositoryName do
  let(:subject) { Class.new { extend MyOrg::GetFullRepositoryName } }

  describe "get_repo_name" do
    it 'has valid name' do
      expect(subject.get_repo_name("chef_test")).to eq("example/chef_test_repository")
    end
  end
end

You don't need ChefSpec here because the content of the library is not Chef (although using node attributes in the library might make things more difficult - I generally find it easier to pass the node attribute in as an argument when writing library functions). There seems to be a lot of mix up between example code and names between our two examples.

Both your questions are about testing ruby code, not chef-specific code. It looks like if you looked up a plain RSpec tutorial it would answer a lot of these concepts.