Module: Halite::SpecHelper

Extended by:
RSpec::SharedContext
Includes:
ChefSpec::API
Defined in:
lib/halite/spec_helper.rb,
lib/halite/spec_helper/runner.rb,
lib/halite/spec_helper/patcher.rb

Overview

A helper module for RSpec tests of resource-based cookbooks.

Examples:

describe MyMixin do
  resource(:my_thing) do
    include Poise
    include MyMixin
    action(:install)
    attribute(:path, kind_of: String, default: '/etc/thing')
  end
  provider(:my_thing) do
    include Poise
    def action_install
      file new_resource.path do
        content new_resource.my_mixin
      end
    end
  end
  recipe do
    my_thing 'test'
  end

  it { is_expected.to create_file('/etc/thing').with(content: 'mixin stuff') }
end

Since:

  • 1.0.0

Defined Under Namespace

Classes: Runner

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#chefspec_optionsHash<Symbol, Object> (readonly)

Options hash for the ChefSpec runner instance.

Examples:

Enable Fauxhai attributes

let(:chefspec_options) { {platform: 'ubuntu', version: '12.04'} }

Returns:

  • (Hash<Symbol, Object>)


95
# File 'lib/halite/spec_helper.rb', line 95

let(:chefspec_options) { Hash.new }

#default_attributesHash (readonly)

Hash to use as default-level node attributes for this example.

Examples:

before do
  default_attributes['myapp']['url'] = 'http://testserver'
end

Returns:

  • (Hash)


77
# File 'lib/halite/spec_helper.rb', line 77

let(:default_attributes) { Hash.new }

#normal_attributesHash (readonly)

Hash to use as normal-level node attributes for this example.

Returns:

  • (Hash)

See Also:



82
# File 'lib/halite/spec_helper.rb', line 82

let(:normal_attributes) { Hash.new }

#override_attributesHash (readonly)

Hash to use as override-level node attributes for this example.

Returns:

  • (Hash)

See Also:



87
# File 'lib/halite/spec_helper.rb', line 87

let(:override_attributes) { Hash.new }

Class Method Details

.described_classObject

Since:

  • 1.0.0



58
# File 'lib/halite/spec_helper.rb', line 58

def self.described_class; nil; end

.provider(name, auto: true, rspec: true, parent: Chef::Provider, patch: true, defined_at: , &block) ⇒ Object

Define a provider class for use in an example group. By default a :run action will be created, load_current_resource will be defined as a no-op, and the RSpec matchers will be available inside the provider.

Examples:

describe MyMixin do
  resource(:my_resource)
  provider(:my_resource) do
    include Poise
    def action_run
      ruby_block 'test'
    end
  end
  recipe do
    my_resource 'test'
  end
  it { is_expected.to run_my_resource('test') }
  it { is_expected.to run_ruby_block('test') }
end

Parameters:

  • name (Symbol)

    Name for the provider in snake-case.

  • options (Hash)

    Provider options.

  • block (Proc)

    Body of the provider class. Optional.

Raises:

Since:

  • 1.0.0



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/halite/spec_helper.rb', line 372

def provider(name, auto: true, rspec: true, parent: Chef::Provider, patch: true, defined_at: caller[0], &block)
  parent = providers[parent] if parent.is_a?(Symbol)
  raise Halite::Error.new("Parent class for #{name} is not a class: #{parent.inspect}") unless parent.is_a?(Class)
  # Pull out the example group for use in the class.
  example_group = self
  # Create the provider class.
  provider_class = Class.new(parent) do
    # Pull in RSpec expectations.
    if rspec
      include RSpec::Matchers
      include RSpec::Mocks::ExampleMethods
    end

    if auto
      # Default blank impl to avoid error.
      def load_current_resource
      end

      # Blank action because I do that so much.
      def action_run
      end
    end

    # Make the anonymous class pretend to have a name.
    define_singleton_method(:name) do
      'Chef::Provider::' + Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s)
    end

    # Helper for debugging, shows where the class was defined.
    define_singleton_method(:halite_defined_at) do
      defined_at
    end

    # Create magic delegators for various metadata.
    {
      example_group: example_group,
      described_class: example_group.[:described_class],
    }.each do |key, value|
      define_method(key) { value }
      define_singleton_method(key) { value }
    end

    # Evaluate the class body.
    class_exec(&block) if block
  end

  # Clean up any global registration that happens on class compile.
  Patcher.post_create_cleanup(name, provider_class) if patch

  # Store for use up with the parent system
  halite_helpers[:providers][name.to_sym] = provider_class

  around do |ex|
    if patch && provider(name) == provider_class
      # We haven't been overridden from a nested scope.
      Patcher.patch(name, provider_class) { ex.run }
    else
      ex.run
    end
  end
end

.recipe(*recipe_names, subject: true, &block) ⇒ Object

Define a recipe to be run via ChefSpec and used as the subject of this example group. You can specify either a single recipe block or one-or-more recipe names.

Examples:

Using a recipe block

describe 'my recipe' do
  recipe do
    ruby_block 'test'
  end
  it { is_expected.to run_ruby_block('test') }
end

Using external recipes

describe 'my recipe' do
  recipe 'my_recipe'
  it { is_expected.to run_ruby_block('test') }
end

Parameters:

  • recipe_names (Array<String>)

    Recipe names to converge for this test.

  • block (Proc)

    Recipe to converge for this test.

  • subject (Boolean) (defaults to: true)

    If true, this recipe should be the subject of this test.

Since:

  • 1.0.0



182
183
184
185
186
# File 'lib/halite/spec_helper.rb', line 182

def recipe(*recipe_names, subject: true, &block)
  # Keep the actual logic in a let in case I want to define the subject as something else
  let(:chef_run) { recipe_names.empty? ? chef_runner.converge_block(&block) : chef_runner.converge(*recipe_names) }
  subject { chef_run } if subject
end

.resource(name, auto: true, parent: Chef::Resource, step_into: true, unwrap_notifying_block: true, patch: true, defined_at: , &block) ⇒ Object

Define a resource class for use in an example group. By default the :run action will be set as the default.

Examples:

describe MyMixin do
  resource(:my_resource) do
    include Poise
    attribute(:path, kind_of: String)
  end
  provider(:my_resource)
  recipe do
    my_resource 'test' do
      path '/tmp'
    end
  end
  it { is_expected.to run_my_resource('test').with(path: '/tmp') }
end

Parameters:

  • name (Symbol)

    Name for the resource in snake-case.

  • options (Hash)

    Resource options.

  • block (Proc)

    Body of the resource class. Optional.

Raises:

Since:

  • 1.0.0



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/halite/spec_helper.rb', line 269

def resource(name, auto: true, parent: Chef::Resource, step_into: true, unwrap_notifying_block: true, patch: true, defined_at: caller[0], &block)
  parent = resources[parent] if parent.is_a?(Symbol)
  raise Halite::Error.new("Parent class for #{name} is not a class: #{parent.inspect}") unless parent.is_a?(Class)
  # Pull out the example group for use in the class.
  example_group = self
  # Create the resource class.
  resource_class = Class.new(parent) do
    # Make the anonymous class pretend to have a name.
    define_singleton_method(:name) do
      'Chef::Resource::' + Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s)
    end

    # Helper for debugging, shows where the class was defined.
    define_singleton_method(:halite_defined_at) do
      defined_at
    end

    # Create magic delegators for various metadata.
    {
      example_group: example_group,
      described_class: example_group.[:described_class],
    }.each do |key, value|
      define_method(key) { value }
      define_singleton_method(key) { value }
    end

    # Evaluate the class body.
    class_exec(&block) if block

    # Optional initialization steps. Disable for special unicorn tests.
    if auto
      # Fill in a :run action by default.
      old_init = instance_method(:initialize)
      define_method(:initialize) do |*args|
        old_init.bind(self).call(*args)
        # Fill in the resource name because I know it, but don't
        # overwrite because a parent might have done this already.
        @resource_name = name.to_sym
        # ChefSpec doesn't seem to work well with action :nothing
        if Array(@action) == [:nothing]
          @action = :run
          @allowed_actions |= [:run]
        end
        if defined?(self.class.default_action) && Array(self.class.default_action) == [:nothing]
          self.class.default_action(:run)
        end
      end
    end
  end

  # Try to set the resource name for 12.4+.
  if defined?(resource_class.resource_name)
    resource_class.resource_name(name)
  end

  # Clean up any global registration that happens on class compile.
  Patcher.post_create_cleanup(name, resource_class) if patch

  # Store for use up with the parent system
  halite_helpers[:resources][name.to_sym] = resource_class

  # Automatically step in to our new resource
  step_into(resource_class, name, unwrap_notifying_block: unwrap_notifying_block) if step_into

  around do |ex|
    if patch && resource(name) == resource_class
      # We haven't been overridden from a nested scope.
      Patcher.patch(name, resource_class) { ex.run }
    else
      ex.run
    end
  end
end

.step_into(name) ⇒ Object .step_into(resource, resource_name) ⇒ Object

Configure ChefSpec to step in to a resource/provider. This will also automatically create ChefSpec matchers for the resource.

Examples:

describe 'my_lwrp' do
  step_into(:my_lwrp)
  recipe do
    my_lwrp 'test'
  end
  it { is_expected.to run_ruby_block('test') }
end

Overloads:

  • .step_into(name) ⇒ Object

    Parameters:

    • name (String, Symbol)

      Name of the resource in snake-case.

  • .step_into(resource, resource_name) ⇒ Object

    Parameters:

    • resource (Class)

      Resource class to step in to.

    • resource_name (String, Symbol, nil)

      Name of the given resource in snake-case.

Since:

  • 1.0.0



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/halite/spec_helper.rb', line 204

def step_into(name=nil, resource_name=nil, unwrap_notifying_block: true)
  return super() if name.nil?
  resource_class = if name.is_a?(Class)
    name
  elsif resources[name.to_sym]
    # Handle cases where the resource has defined via a helper with
    # step_into:false but a nested example wants to step in.
    resources[name.to_sym]
  else
    # Won't see platform/os specific resources but not sure how to fix
    # that. I need the class here for the matcher creation below.
    Chef::Resource.resource_for_node(name.to_sym, Chef::Node.new)
  end
  resource_name ||= if resource_class.respond_to?(:resource_name)
    resource_class.resource_name
  else
    Chef::Mixin::ConvertToClassName.convert_to_snake_case(resource_class.name.split('::').last)
  end

  # Patch notifying_block from Poise::Provider to just run directly.
  # This is not a great solution but it is better than nothing for right
  # now. In the future this should maybe do an internal converge but using
  # ChefSpec somehow?
  if unwrap_notifying_block
    old_provider_for_action = resource_class.instance_method(:provider_for_action)
    resource_class.send(:define_method, :provider_for_action) do |*args|
      old_provider_for_action.bind(self).call(*args).tap do |provider|
        if provider.respond_to?(:notifying_block, true)
          provider.define_singleton_method(:notifying_block) do |&block|
            block.call
          end
        end
      end
    end
  end

  # Add to the let variable passed in to ChefSpec.
  super(resource_name)
end

Instance Method Details

#chef_runner_classObject

Custom runner class.

Since:

  • 1.0.0



118
119
120
# File 'lib/halite/spec_helper.rb', line 118

def chef_runner_class
  Halite::SpecHelper::Runner
end

#chef_runner_optionsObject

Merge in extra options data.

Since:

  • 1.0.0



106
107
108
109
110
111
112
113
114
115
# File 'lib/halite/spec_helper.rb', line 106

def chef_runner_options
  super.tap do |options|
    options[:halite_gemspec] = halite_gemspec
    # And some legacy data.
    options[:default_attributes].update(default_attributes)
    options[:normal_attributes].update(normal_attributes)
    options[:override_attributes].update(override_attributes)
    options.update(chefspec_options)
  end
end

#run_chefObject

An alias for slightly more semantic meaning, just forces the lazy #subject to run.

Examples:

describe 'my recipe' do
  recipe 'my_recipe'
  it { run_chef }
end

See Also:

Since:

  • 1.0.0



136
137
138
# File 'lib/halite/spec_helper.rb', line 136

def run_chef
  chef_run
end