ChefSpec - Is there anyway to change the node attributes for different contexts?

Hi I have a ChefSpec test below, I have looked around on Google but could not find the best way to manipulate the node attributes (from recipe) in different context blocks in ChefSpec file. Please let me know if there is a better way to change the node attributes in 'context' level or 'it' block level?

require 'spec_helper'

iis = {
    sites: [
        {.....,
        app_pools: [....]
        }
    ]
}

iis_override = {
    site: {...}
}
    
describe 'myCookbook::iis' do

    let(:chef_run) do
        ChefSpec::SoloRunner.new do |node|
            node.default['is_dev_machine'] = false
        end.converge(described_recipe)
        # Case 1: node['iis_override'] doesnt exist
    end

    # There are 2 cases here:
    # 1: node['iis_override'] exists or not null
    # 2: node['iis_override'] does not exist
    iis[:sites].each do |site|  
        context "if node['iis_override'] DOESNT exist" do 
            it 'adds the default app pool' do
                expect(chef_run).to add_iis_pool('DefaultAppPool').with(
                    ...
                )
            end
            ## More complicated 'it' blocks below
        end

        context "if node['iis_override'] DOES exist" do 
            #How to correctly set node attribute for iis_override?
            #These 2 lines NOT WORKING because they only work in 'it' block
            #However, I want to change the node attribute in the context level because 
            # I have many 'it' blocks under this context and dont want to duplicate this process
            chef_run.node.normal['iis_override'] = iis_override
            chef_run.converge(described_recipe)

            it 'adds the default app pool' do
                expect(chef_run).to add_iis_pool('DefaultAppPool').with(
                    ...
                )
            end
            ## More complicated 'it' blocks below similar to the previous context
        end
    end
end

Thanks

What version of ChefSpec are you using? With v7+, test setup has become much easier to write and read. For setting node attributes at different levels in different contexts, see the ChefSpec README section on Node Attributes.

Here's my attempt at a ChefSpec v7 refresh of your tests:

require 'spec_helper'

iis = {
    sites: [
        {.....,
        app_pools: [....]
        }
    ]
}

iis_override = {
    site: {...}
}
    
describe 'myCookbook::iis' do
  # the Chef run is implied, no need to set it up with the let
  platform 'ubuntu' # or whatever your target platform is
  default_attributes['is_dev_machine'] = false

  iis[:sites].each do |site|
    # recommend another context to represent each site as
    # the collection of sites is iterated over
    context "for #{site['something']['identifiable']}" do

      context "when iis_override is not set (default)" do 
        it 'adds the default app pool' do
          is_expected.to add_iis_pool('DefaultAppPool').with(
            ...
           )
         end
         ## More complicated 'it' blocks below
       end

      context "when iis_override is set" do
        normal_attrbutes['iis_override'] = iis_override

        it 'adds the default app pool' do
          is_expected.to add_iis_pool('DefaultAppPool').with(
            ...
          )
        end
      end

    end
  end
end

Thanks for your reply, your solution works as what I expected, but it seems to cause another problem when I tried to mock out some library functions in this test.

# spec/spec_helper.rb
require 'chefspec'
require 'chefspec/cacher'
ChefSpec::Coverage

# Require all our libraries
Dir['libraries/*.rb'].each { |f| require File.expand_path(f) }
Dir['../azure_keyvault/libraries/*.rb'].each { |f| require File.expand_path(f) }

RSpec.configure do |config|
    config.include MyOrg::GetFullServiceAccountName
    config.include MyOrg::GetServiceAccountPassword
    config.include MyOrg::GetKeyVaultSecret
    config.include Azure::KeyVault
    config.cookbook_path = ['../../cookbooks', '../../external-cookbooks']
    config.platform = 'windows'
    config.version = '2016'
end

This is the chefspec file. Because my ChefSpec file is quite large and I forgot to add some other logic in the original thread, now the error seems to be thrown from this logic, so I will add them in this simple Chefspec example.

require 'spec_helper'

iis = {
    sites: [
        {.....,
        app_pools: [....]
        }
    ],
    https_certificate: {
        identifier: "12345",
        identifier_type: "Thumbprint"
    }
}

iis_override = {
    site: {...}
}
    
describe 'myCookbook::iis' do
  # the Chef run is implied, no need to set it up with the let
  #This platform line is not working and throwing error saying that some of my iis resources are not found 
  #when I executed the ChefSpec test  
  #platform 'windows' 
    default_attributes['is_dev_machine'] = false

    iis[:sites].each do |site|
        # I dont think I will place a context block here since I got there are more in iis variable
        # and iis_override gonna place them later in the context block
        # context "for #{site['something']['identifiable']}" do

        context "when iis_override is not set (default)" do 
            #This mocking block was working fine before I converted your solution
            #Whenever I added another 'it' blocks (see at the end), it failed because it tried to 
            # step into this library function, even though I have already mocked this function with a 
            #return value (which I believe it should not step into the function because I have already mocked it). 
            #However, if I removed any another 'it' blocks, it seems not to step into the library function
            # and the mocking is fine.
            before do 
                allow_any_instance_of(Chef::Recipe)
                    .to receive(:get_service_account_password)
                    .with(site[:app][:permissions][:base_username]) 
                    .and_return("test_password")
            end  

            it 'adds the default app pool' do
            is_expected.to add_iis_pool('DefaultAppPool').with(
                ...
            )
            end
            ## More complicated 'it' blocks below
        end

        context "when iis_override is set" do
            normal_attrbutes['iis_override'] = iis_override
            before do 
                allow_any_instance_of(Chef::Recipe)
                    .to receive(:get_service_account_password)
                    .with(site[:app][:permissions][:base_username]) 
                    .and_return("test_password")
            end 

            it 'adds the default app pool' do
            is_expected.to add_iis_pool('DefaultAppPool').with(
                ...
            )
            end
        end

        end
    end

    #With your solution, everything worked fine without this below test, it was working before I converted to this solution, 
    #not sure why, but the error isnt about this 'it' block, it is about the mocking block above when it tried to 
    #step into the library function (which it isn't supposed to step into) and throw some errors because of 
    #some missing variables.
    it "creates windows_certificate_binding" do
        cert_identifier = iis[:https_certificate][:identifier]
        cert_identifier_type = mapped_cert_binding_identifier_type[iis[:https_certificate][:identifier_type]]

        is_expected.to create_windows_certificate_binding("Bind IIS HTTPS Certificate").with(
            cert_name:  cert_identifier,
            name_kind:  cert_identifier_type,
            address:    '0.0.0.0',
            port:       443
        )
    end

end

This is the library I got:

module Enett
    module GetServiceAccountPassword
        def get_service_account_password(base_username)

            service_account_name = "#{node['service_account_prefixes'][node['server_type']]}_#{base_username}"

            service_account_password = get_key_vault_secret(service_account_name.gsub("_","-").downcase, node['key_vault_data_bag'])

        end
    end
end
Chef::Recipe.send(:include, MyOrg::GetServiceAccountPassword)

I hope you could give me some more advice and suggestions on this since I have spent the whole day trying to debug this but still havent got any luck. If I go back to my original solution, it would fix the mocking problem but then I cant change node attributes across different context blocks.

To simplify the previous comment, I just found out that whenever I place context block, the mocking function is not working properly.

context "when iis_override is not set (default)" do 
            #This mocking block is not working properly inside the context block.
            #If I move everything out of this context block, everything works fine
            # including the mocking library function below. 
            # The error is basically it tried to step into the library function,
            # even though its not supposed to as I have mocked it out 
            # Is there a different way of mocking library function 
            #specifically for outside and inside context block??
            before do 
                allow_any_instance_of(Chef::Recipe)
                    .to receive(:get_service_account_password)
                    .with(site[:app][:permissions][:base_username]) 
                    .and_return("test_password")
            end  

            it 'adds the default app pool' do
            is_expected.to add_iis_pool('DefaultAppPool').with(
                ...
            )
            end
            ## More complicated 'it' blocks below
        end