Skip to content
This repository
  • 19 commits
  • 15 files changed
  • 0 comments
  • 2 contributors
216 README.md
Source Rendered
... ... @@ -1,4 +1,4 @@
1   -# RSpec tests for your Puppet manifests
  1 +# RSpec tests for your Puppet manifests & modules
2 2
3 3 ## Installation
4 4
@@ -24,50 +24,80 @@ structure and naming convention.
24 24 | +-- <class_name>_spec.rb
25 25 |
26 26 +-- defines
  27 + | |
  28 + | +-- <define_name>_spec.rb
  29 + |
  30 + +-- functions
27 31 |
28   - +-- <define_name>_spec.rb
  32 + +-- <function_name>_spec.rb
29 33
30 34 ## Example groups
31 35
32 36 If you use the above directory structure, your examples will automatically be
33   -placed in the correct groups and have access to the custom matchers. If you
34   -choose not to, you can force the examples into the required groups as follows.
  37 +placed in the correct groups and have access to the custom matchers. *If you
  38 +choose not to*, you can force the examples into the required groups as follows.
  39 +
  40 +```ruby
  41 +describe 'myclass', :type => :class do
  42 + ...
  43 +end
35 44
36   - describe 'myclass', :type => :class do
37   - ...
38   - end
  45 +describe 'mydefine', :type => :define do
  46 + ...
  47 +end
39 48
40   - describe 'mydefine', :type => :define do
41   - ...
42   - end
  49 +describe 'myfunction', :type => :puppet_function do
  50 + ...
  51 +end
  52 +```
43 53
44   -## Matchers
  54 +## Defined Types & Classes
45 55
46   -### Checking if a class has been included
  56 +### Matchers
  57 +
  58 +#### Checking if a class has been included
47 59
48 60 You can test if a class has been included in the catalogue with the
49 61 `include_class` matcher. It takes the class name as a string as its only
50 62 argument
51 63
52   - it { should include_class('foo') }
  64 +```ruby
  65 +it { should include_class('foo') }
  66 +```
53 67
54   -### Checking if a resources exists
  68 +#### Checking if a resource exists
55 69
56 70 You can test if a resource exists in the catalogue with the generic
57   -`creates_<resource type>` matcher. If your resource type includes :: (e.g.
  71 +`contain_<resource type>` matcher.
  72 +
  73 +```ruby
  74 +it { should contain_augeas('bleh') }
  75 +```
  76 +
  77 +If your resource type includes :: (e.g.
58 78 `foo::bar` simply replace the :: with __ (two underscores).
59 79
60   - it { should contain_augeas('bleh') }
61   - it { should contain_foo__bar('baz') }
  80 +```ruby
  81 +it { should contain_foo__bar('baz') }
  82 +```
62 83
63 84 You can further test the parameters that have been passed to the resources with
64 85 the generic `with_<parameter>` chains.
65 86
66   - it { should contain_package('mysql-server').with_ensure('present') }
  87 +```ruby
  88 +it { should contain_package('mysql-server').with_ensure('present') }
  89 +```
  90 +
  91 +You can also test that specific parameters have been left undefined with the
  92 +generic `without_<parameter>` chains.
67 93
68   -## Writing tests
  94 +```ruby
  95 +it { should contain_file('/foo/bar').without_mode }
  96 +```
69 97
70   -### Basic test structure
  98 +### Writing tests
  99 +
  100 +#### Basic test structure
71 101
72 102 To test that
73 103
@@ -81,46 +111,154 @@ Will cause the following resource to be in included in catalogue for a host
81 111 command => '/sbin/sysctl -p /etc/sysctl.conf',
82 112 }
83 113
84   -We can write the following testcase
  114 +We can write the following testcase (in `spec/defines/sysctl_spec.rb`)
85 115
86   - describe 'sysctl' do
87   - let(:title) { 'baz' }
88   - let(:params) { { :value => 'foo' } }
  116 +```ruby
  117 +describe 'sysctl' do
  118 + let(:title) { 'baz' }
  119 + let(:params) { { :value => 'foo' } }
89 120
90   - it { should contain_exec('sysctl/reload').with_command("/sbin/sysctl -p /etc/sysctl.conf") }
91   - end
  121 + it { should contain_exec('sysctl/reload').with_command("/sbin/sysctl -p /etc/sysctl.conf") }
  122 +end
  123 +```
92 124
93   -### Specifying the title of a resource
  125 +#### Specifying the title of a resource
94 126
95   - let(:title) { 'foo' }
  127 +```ruby
  128 +let(:title) { 'foo' }
  129 +```
96 130
97   -### Specifying the parameters to pass to a resources or parametised class
  131 +#### Specifying the parameters to pass to a resources or parametised class
98 132
99   - let(:params) { {:ensure => 'present', ...} }
  133 +```ruby
  134 +let(:params) { {:ensure => 'present', ...} }
  135 +```
100 136
101   -### Specifying the FQDN of the test node
  137 +#### Specifying the FQDN of the test node
102 138
103 139 If the manifest you're testing expects to run on host with a particular name,
104 140 you can specify this as follows
105 141
106   - let(:node) { 'testhost.example.com' }
  142 +```ruby
  143 +let(:node) { 'testhost.example.com' }
  144 +```
107 145
108   -### Specifying the facts that should be available to your manifest
  146 +#### Specifying the facts that should be available to your manifest
109 147
110 148 By default, the test environment contains no facts for your manifest to use.
111 149 You can set them with a hash
112 150
113   - let(:facts) { {:operatingsystem => 'Debian', :kernel => 'Linux', ...} }
  151 +```ruby
  152 +let(:facts) { {:operatingsystem => 'Debian', :kernel => 'Linux', ...} }
  153 +```
114 154
115   -### Specifying the path to find your modules
  155 +#### Specifying the path to find your modules
116 156
117 157 I recommend setting a default module path by adding the following code to your
118 158 `spec_helper.rb`
119 159
120   - RSpec.configure do |c|
121   - c.module_path = '/path/to/your/module/dir'
122   - end
  160 +```ruby
  161 +RSpec.configure do |c|
  162 + c.module_path = '/path/to/your/module/dir'
  163 +end
  164 +```
123 165
124 166 However, if you want to specify it in each example, you can do so
125 167
126   - let(:module_path) { '/path/to/your/module/dir' }
  168 +```ruby
  169 +let(:module_path) { '/path/to/your/module/dir' }
  170 +```
  171 +
  172 +## Functions
  173 +
  174 +### Matchers
  175 +
  176 +All of the standard RSpec matchers are available for you to use when testing
  177 +Puppet functions.
  178 +
  179 +```ruby
  180 +it 'should be able to do something' do
  181 + subject.call('foo') == 'bar'
  182 +end
  183 +```
  184 +
  185 +For your convenience though, a `run` matcher exists to provide easier to
  186 +understand test cases.
  187 +
  188 +```ruby
  189 +it { should run.with_params('foo').and_return('bar') }
  190 +```
  191 +
  192 +### Writing tests
  193 +
  194 +#### Basic test structure
  195 +
  196 +```ruby
  197 +require 'spec_helper'
  198 +
  199 +describe '<function name>' do
  200 + ...
  201 +end
  202 +```
  203 +
  204 +#### Specifying the name of the function to test
  205 +
  206 +The name of the function must be provided in the top level description, e.g.
  207 +
  208 +```ruby
  209 +describe 'split' do
  210 +```
  211 +
  212 +#### Specifying the arguments to pass to the function
  213 +
  214 +You can specify the arguments to pass to your function during the test(s) using
  215 +either the `with_params` chain method in the `run` matcher
  216 +
  217 +```ruby
  218 +it { should run.with_params('foo', 'bar', ['baz']) }
  219 +```
  220 +
  221 +Or by using the `call` method on the subject directly
  222 +
  223 +```ruby
  224 +it 'something' do
  225 + subject.call('foo', 'bar', ['baz'])
  226 +end
  227 +```
  228 +
  229 +#### Testing the results of the function
  230 +
  231 +You can test the result of a function (if it produces one) using either the
  232 +`and_returns` chain method in the `run` matcher
  233 +
  234 +```ruby
  235 +it { should run.with_params('foo').and_return('bar') }
  236 +```
  237 +
  238 +Or by using any of the existing RSpec matchers on the subject directly
  239 +
  240 +```ruby
  241 +it 'something' do
  242 + subject.call('foo') == 'bar'
  243 + subject.call('baz').should be_an Array
  244 +end
  245 +```
  246 +
  247 +#### Testing the errors thrown by the function
  248 +
  249 +You can test whether the function throws an exception using either the
  250 +`and_raises_error` chain method in the `run` matcher
  251 +
  252 +```ruby
  253 +it { should run.with_params('a', 'b').and_raise_error(Puppet::ParseError) }
  254 +it { should_not run.with_params('a').and_raise_error(Puppet::ParseError) }
  255 +```
  256 +
  257 +Or by using the existing `raises_error` RSpec matcher
  258 +
  259 +```ruby
  260 +it 'something' do
  261 + expect { subject.call('a', 'b') }.should raise_error(Puppet::ParseError)
  262 + expect { subject.call('a') }.should_not raise_error(Puppet::ParseError)
  263 +end
  264 +```
3  lib/rspec-puppet.rb
@@ -4,4 +4,7 @@
4 4
5 5 RSpec.configure do |c|
6 6 c.add_setting :module_path, :default => '/etc/puppet/modules'
  7 + c.add_setting :manifest_dir, :default => nil
  8 + c.add_setting :manifest, :default => nil
  9 + c.add_setting :template_dir, :default => nil
7 10 end
5 lib/rspec-puppet/example.rb
... ... @@ -1,6 +1,7 @@
1 1 require 'rspec-puppet/support'
2 2 require 'rspec-puppet/example/define_example_group'
3 3 require 'rspec-puppet/example/class_example_group'
  4 +require 'rspec-puppet/example/function_example_group'
4 5
5 6 RSpec::configure do |c|
6 7 def c.escaped_path(*parts)
@@ -14,4 +15,8 @@ def c.escaped_path(*parts)
14 15 c.include RSpec::Puppet::ClassExampleGroup, :type => :class, :example_group => {
15 16 :file_path => c.escaped_path(%w[spec classes])
16 17 }
  18 +
  19 + c.include RSpec::Puppet::FunctionExampleGroup, :type => :puppet_function, :example_group => {
  20 + :file_path => c.escaped_path(%w[spec functions])
  21 + }
17 22 end
14 lib/rspec-puppet/example/class_example_group.rb
... ... @@ -1,6 +1,6 @@
1 1 module RSpec::Puppet
2 2 module ClassExampleGroup
3   - include RSpec::Puppet::Matchers
  3 + include RSpec::Puppet::ManifestMatchers
4 4 include RSpec::Puppet::Support
5 5
6 6 def subject
@@ -9,6 +9,9 @@ def subject
9 9
10 10 def catalogue
11 11 Puppet[:modulepath] = self.respond_to?(:module_path) ? module_path : RSpec.configuration.module_path
  12 + Puppet[:manifestdir] = self.respond_to?(:manifest_dir) ? manifest_dir : RSpec.configuration.manifest_dir
  13 + Puppet[:manifest] = self.respond_to?(:manifest) ? manifest : RSpec.configuration.manifest
  14 + Puppet[:templatedir] = self.respond_to?(:template_dir) ? template_dir : RSpec.configuration.template_dir
12 15
13 16 klass_name = self.class.top_level_description.downcase
14 17
@@ -17,6 +20,8 @@ def catalogue
17 20 if File.exists?(File.join(Puppet[:modulepath], 'manifests', 'init.pp'))
18 21 path_to_manifest = File.join([Puppet[:modulepath], 'manifests', klass_name.split('::')[1..-1]].flatten)
19 22 import_str = "import '#{Puppet[:modulepath]}/manifests/init.pp'\nimport '#{path_to_manifest}.pp'\n"
  23 + elsif File.exists?(Puppet[:modulepath])
  24 + import_str = "import '#{Puppet[:manifest]}'\n"
20 25 else
21 26 import_str = ""
22 27 end
@@ -36,7 +41,12 @@ def catalogue
36 41 Puppet[:code] = pre_cond + "\n" + Puppet[:code]
37 42
38 43 nodename = self.respond_to?(:node) ? node : Puppet[:certname]
39   - facts_val = self.respond_to?(:facts) ? facts : {}
  44 + facts_val = {
  45 + 'hostname' => nodename.split('.').first,
  46 + 'fqdn' => nodename,
  47 + 'domain' => nodename.split('.').last,
  48 + }
  49 + facts_val.merge!(munge_facts(facts)) if self.respond_to?(:facts)
40 50
41 51 build_catalog(nodename, facts_val)
42 52 end
10 lib/rspec-puppet/example/define_example_group.rb
... ... @@ -1,6 +1,6 @@
1 1 module RSpec::Puppet
2 2 module DefineExampleGroup
3   - include RSpec::Puppet::Matchers
  3 + include RSpec::Puppet::ManifestMatchers
4 4 include RSpec::Puppet::Support
5 5
6 6 def subject
@@ -11,12 +11,17 @@ def catalogue
11 11 define_name = self.class.top_level_description.downcase
12 12
13 13 Puppet[:modulepath] = self.respond_to?(:module_path) ? module_path : RSpec.configuration.module_path
  14 + Puppet[:manifestdir] = self.respond_to?(:manifest_dir) ? manifest_dir : RSpec.configuration.manifest_dir
  15 + Puppet[:manifest] = self.respond_to?(:manifest) ? manifest : RSpec.configuration.manifest
  16 + Puppet[:templatedir] = self.respond_to?(:template_dir) ? template_dir : RSpec.configuration.template_dir
14 17
15 18 # If we're testing a standalone module (i.e. one that's outside of a
16 19 # puppet tree), the autoloader won't work, so we need to fudge it a bit.
17 20 if File.exists?(File.join(Puppet[:modulepath], 'manifests', 'init.pp'))
18 21 path_to_manifest = File.join([Puppet[:modulepath], 'manifests', define_name.split('::')[1..-1]].flatten)
19 22 import_str = "import '#{Puppet[:modulepath]}/manifests/init.pp'\nimport '#{path_to_manifest}.pp'\n"
  23 + elsif File.exists?(Puppet[:modulepath])
  24 + import_str = "import '#{Puppet[:manifest]}'\n"
20 25 else
21 26 import_str = ""
22 27 end
@@ -41,8 +46,9 @@ def catalogue
41 46 facts_val = {
42 47 'hostname' => nodename.split('.').first,
43 48 'fqdn' => nodename,
  49 + 'domain' => nodename.split('.', 2).last,
44 50 }
45   - facts_val.merge!(facts) if self.respond_to?(:facts)
  51 + facts_val.merge!(munge_facts(facts)) if self.respond_to?(:facts)
46 52
47 53 build_catalog(nodename, facts_val)
48 54 end
17 lib/rspec-puppet/example/function_example_group.rb
... ... @@ -0,0 +1,17 @@
  1 +module RSpec::Puppet
  2 + module FunctionExampleGroup
  3 + include RSpec::Puppet::FunctionMatchers
  4 +
  5 + def subject
  6 + function_name = self.class.top_level_description.downcase
  7 +
  8 + Puppet[:modulepath] = self.respond_to?(:module_path) ? module_path : RSpec.configuration.module_path
  9 + Puppet[:libdir] = Dir["#{Puppet[:modulepath]}/*/lib"].entries.join(File::PATH_SEPARATOR)
  10 + Puppet::Parser::Functions.autoloader.loadall
  11 +
  12 + scope = Puppet::Parser::Scope.new
  13 +
  14 + scope.method "function_#{function_name}".to_sym
  15 + end
  16 + end
  17 +end
1  lib/rspec-puppet/matchers.rb
... ... @@ -1,3 +1,4 @@
1 1 require 'rspec-puppet/matchers/create_generic'
2 2 require 'rspec-puppet/matchers/create_resource'
3 3 require 'rspec-puppet/matchers/include_class'
  4 +require 'rspec-puppet/matchers/run'
17 lib/rspec-puppet/matchers/create_generic.rb
... ... @@ -1,5 +1,5 @@
1 1 module RSpec::Puppet
2   - module Matchers
  2 + module ManifestMatchers
3 3 class CreateGeneric
4 4 def initialize(*args, &block)
5 5 @exp_resource_type = args.shift.to_s.gsub(/^(create|contain)_/, '')
@@ -14,6 +14,10 @@ def method_missing(method, *args, &block)
14 14 param = method.to_s.gsub(/^with_/, '')
15 15 (@expected_params ||= []) << [param, args[0]]
16 16 self
  17 + elsif method.to_s =~ /^without_/
  18 + param = method.to_s.gsub(/^without_/, '')
  19 + (@expected_undef_params ||= []) << param
  20 + self
17 21 else
18 22 super
19 23 end
@@ -34,6 +38,15 @@ def matches?(catalogue)
34 38 end
35 39 end
36 40 end
  41 +
  42 + if @expected_undef_params
  43 + @expected_undef_params.each do |name|
  44 + unless resource.send(:parameters)[name.to_sym].nil?
  45 + ret = false
  46 + (@errors ||= []) << "#{name.to_s} undefined"
  47 + end
  48 + end
  49 + end
37 50 end
38 51
39 52 ret
@@ -63,7 +76,7 @@ def errors
63 76 end
64 77
65 78 def method_missing(method, *args, &block)
66   - return RSpec::Puppet::Matchers::CreateGeneric.new(method, *args, &block) if method.to_s =~ /^(create|contain)_/
  79 + return RSpec::Puppet::ManifestMatchers::CreateGeneric.new(method, *args, &block) if method.to_s =~ /^(create|contain)_/
67 80 super
68 81 end
69 82 end
2  lib/rspec-puppet/matchers/create_resource.rb
... ... @@ -1,5 +1,5 @@
1 1 module RSpec::Puppet
2   - module Matchers
  2 + module ManifestMatchers
3 3 extend RSpec::Matchers::DSL
4 4
5 5 matcher :create_resource do |expected_type, expected_title|
2  lib/rspec-puppet/matchers/include_class.rb
... ... @@ -1,5 +1,5 @@
1 1 module RSpec::Puppet
2   - module Matchers
  2 + module ManifestMatchers
3 3 extend RSpec::Matchers::DSL
4 4
5 5 matcher :include_class do |expected_class|
76 lib/rspec-puppet/matchers/run.rb
... ... @@ -0,0 +1,76 @@
  1 +module RSpec::Puppet
  2 + module FunctionMatchers
  3 + extend RSpec::Matchers::DSL
  4 +
  5 + matcher :run do
  6 + match do |func_obj|
  7 + if @params
  8 + @func = lambda { func_obj.call(@params) }
  9 + else
  10 + @func = lambda { func_obj.call }
  11 + end
  12 +
  13 + if @expected_error
  14 + begin
  15 + @func.call
  16 + rescue @expected_error
  17 + #XXX check error string here
  18 + true
  19 + rescue
  20 + false
  21 + end
  22 + else
  23 + if @expected_return
  24 + @func.call == @expected_return
  25 + else
  26 + begin
  27 + @func.call
  28 + rescue
  29 + false
  30 + end
  31 + true
  32 + end
  33 + end
  34 + end
  35 +
  36 + chain :with_params do |*params|
  37 + @params = params
  38 + end
  39 +
  40 + chain :and_return do |value|
  41 + @expected_return = value
  42 + end
  43 +
  44 + # XXX support error string and regexp
  45 + chain :and_raise_error do |value|
  46 + @expected_error = value
  47 + end
  48 +
  49 + failure_message_for_should do |func_obj|
  50 + func_name = func_obj.name.gsub(/^function_/, '')
  51 + func_params = @params.inspect[1..-2]
  52 +
  53 + if @expected_return
  54 + "expected #{func_name}(#{func_params}) to have returned #{@expected_return.inspect} instead of #{@func.call.inspect}"
  55 + elsif @expected_error
  56 + "expected #{func_name}(#{func_params}) to have raised #{@expected_error.inspect}"
  57 + else
  58 + "expected #{func_name}(#{func_params}) to have run successfully"
  59 + end
  60 + end
  61 +
  62 + failure_message_for_should_not do |func_obj|
  63 + func_name = func_obj.name.gsub(/^function_/, '')
  64 + func_params = @params.inspect[1..-2]
  65 +
  66 + if @expected_return
  67 + "expected #{func_name}(#{func_params}) to not have returned #{@expected_return.inspect}"
  68 + elsif @expected_error
  69 + "expected #{func_name}(#{func_params}) to not have raised #{@expected_error.inspect}"
  70 + else
  71 + "expected #{func_name}(#{func_params}) to not have run successfully"
  72 + end
  73 + end
  74 + end
  75 + end
  76 +end
6 lib/rspec-puppet/support.rb
@@ -12,5 +12,11 @@ def build_catalog nodename, facts_val
12 12 Puppet::Resource::Catalog.indirection.find(node_obj.name, :use_node => node_obj)
13 13 end
14 14 end
  15 +
  16 + def munge_facts(facts)
  17 + output = {}
  18 + facts.keys.each { |key| output[key.to_s] = facts[key] }
  19 + output
  20 + end
15 21 end
16 22 end
5 rspec-puppet.gemspec
... ... @@ -1,6 +1,6 @@
1 1 Gem::Specification.new do |s|
2 2 s.name = 'rspec-puppet'
3   - s.version = '0.0.6'
  3 + s.version = '0.0.10'
4 4 s.homepage = 'https://github.com/rodjek/rspec-puppet/'
5 5 s.summary = 'RSpec tests for your Puppet manifests'
6 6 s.description = 'RSpec tests for your Puppet manifests'
@@ -8,10 +8,12 @@ Gem::Specification.new do |s|
8 8 s.files = [
9 9 'lib/rspec-puppet/example/class_example_group.rb',
10 10 'lib/rspec-puppet/example/define_example_group.rb',
  11 + 'lib/rspec-puppet/example/function_example_group.rb',
11 12 'lib/rspec-puppet/example.rb',
12 13 'lib/rspec-puppet/matchers/create_generic.rb',
13 14 'lib/rspec-puppet/matchers/create_resource.rb',
14 15 'lib/rspec-puppet/matchers/include_class.rb',
  16 + 'lib/rspec-puppet/matchers/run.rb',
15 17 'lib/rspec-puppet/matchers.rb',
16 18 'lib/rspec-puppet/support.rb',
17 19 'lib/rspec-puppet.rb',
@@ -25,6 +27,7 @@ Gem::Specification.new do |s|
25 27 'spec/defines/sysctl_spec.rb',
26 28 'spec/fixtures/boolean/manifests/init.pp',
27 29 'spec/fixtures/sysctl/manifests/init.pp',
  30 + 'spec/functions/split_spec.rb',
28 31 'spec/spec_helper.rb',
29 32 ]
30 33
3  spec/defines/sysctl_spec.rb
@@ -9,5 +9,6 @@
9 9 .with_context('/files/etc/sysctl.conf') \
10 10 .with_changes("set vm.swappiness '60'") \
11 11 .with_onlyif("match vm.swappiness[.='60'] size == 0") \
12   - .with_notify('Exec[sysctl/reload]') }
  12 + .with_notify('Exec[sysctl/reload]')\
  13 + .without_foo }
13 14 end
11 spec/functions/split_spec.rb
... ... @@ -0,0 +1,11 @@
  1 +require 'spec_helper'
  2 +
  3 +describe 'split' do
  4 + it { should run.with_params('aoeu', 'o').and_return(['a', 'eu']) }
  5 + it { should run.with_params('foo').and_raise_error(Puppet::ParseError) }
  6 + it { should_not run.with_params('foo').and_raise_error(Puppet::DevError) }
  7 +
  8 + it 'something' do
  9 + expect { subject.call('foo') }.should raise_error(Puppet::ParseError)
  10 + end
  11 +end

No commit comments for this range

Something went wrong with that request. Please try again.