I've created a gem for this - rspec-puppet-utils - that includes a MockFunction class, a TemplateHarness class for testing templates, and a HierData::Validator class for checking yaml files. I've refactored this sample project to use rspec-puppet-utils
Take a look at the foo::bar class (modules/foo/manifests/bar.pp), if you want to unit test this class there are a few dependencies that ideally we'd like to mock:
This function is defined within the foo module, but we don't want to test its functionality in the tests for foo::bar, we would write a separate spec for this function
There are a few ways of handling Hiera with rspec-puppet and rspec-hiera-puppet, really hiera is just another function but we still need to get some values out of it.
I haven't included one here, but a prime example would be using a function from stdlib. There are ways (like librarian puppet) of bringing down other modules during your tests, but again, ideally we only want to test this specific class and not any functions that the class depends on.
foo::baz defined type (modules/foo/manifests/baz.pp)
This is defined within the foo module so there's not a problem with it being missing, but baz references a class from another module. This isn't an ideal thing to do, but I think that sometimes it's necessary!?
Regardless of whether foo::baz contains classes from another module, classes from the same module, or no other classes at all, we still don't want to be testing foo::baz in the spec for foo::bar.
In this case foo::dependency is also in the foo module, but it's something that needs to be in the catalogue otherwise Puppet will throw an error. foo::dependency could also easily be other::dependency (again, not ideal, but possible).
See the spec for the bar class (modules/foo/spec/classes/bar_spec.rb). This has examples of all of the following, but there are two key parts:
This is where we mock out the other classes or defined types
define foo::baz ($param1, $param2) {}creates a mockfoo::bazdefined type and overrides the existing one from the moduledefine foo::dependency {}creates a mockfoo::dependencyclassfoo::dependency { "need me": }adds a new "instance" offoo::dependencyto the catalogue to satisfy therequirerelationship
This is nothing new, there are examples of let(:pre_condition) all over the place, I included them here to create a complete example
The MockFunction class comes from rspec-puppet-utils. Internally it calls Puppet::Parser::Functions.newfunction() If it looks familiar, that's because this is how you write custom functions for puppet. The difference is that all the new function does is call the call method on your MockFunction object and return the result.
So for example, if the class you're testing calls my_func('a_string', 3) and expects to get 'penguin' in return (it's a weird function I know but just run with it!) then you can mock this by doing:
MockFunction.new('my_func') { |f|
f.stubs(:call).with(['a_string', 3]).returns('penguin')
}Note that all mock functions take one parameter, which is an array of values, like an array of args funnily enough!
hiera is just another function so mock it like so:
MockFunction.new('hiera') { |f|
f.stubs(:call).raises(Puppet::ParseError.new('Key not found'))
f.stubs(:call).with(['my-key']).returns('badger')
}The block is optional but allows you to setup default behavior, like throwing an error for a key you're not expecting. Note that the error message isn't exactly the same as the one that the real hiera would thrown!
The spec for does_something modules/foo/spec/functions/does_something_spec.rb has a few examples of getting hold of return values, mocking internal function calls, and mocking lookupvar() for getting facts
To get this running for another module:
- add
puppetlabs_spec_helperto your Gemfile (or gem install) - add
rspec-puppet-utilsto your Gemfile (or gem install) - run
rspec-puppet-initin the module root as you would normally - replace the
spec_helper.rbfile with the one fromfoo - replace the module's
Rakefilefile with the one fromfoo - copy the
Rakefilefrom the root of this project (if you want to use it)
I think that's it!? puppetlabs_spec_helper provides a few things:
- one if its dependencies is
mochawhich provides thestubs().with().returns()stuff - it has some nice inbuilt
raketasks likehelp,spec_prep,spec_clean, etc which I'll probably make more use of as my tests become more complex - provides a
scopeobject that you can hook into for testing functions (example coming soon)
Almost forgot, you can run this if you want, just clone the repo, cd into the foo directory, and run rake rspec (or just rake as rspec is the default task). If you play around with it and manage to break it let me know, this is all new so I haven't had a chance to properly test it against loads of scenarios or the rspec-puppet matchers (the should things, whatever they're called).
Edit: I did say to run rake spec above, but really you should run rake rspec. The spec task is provided by puppetlabs_spec_helper/rake_tasks along with a couple of others, by default it cleans up your fixtures dir, which can be useful (and you can use the spec_prep and spec_clean yourself if you want), but it also deletes your site.pp file which breaks these tests!
I've also put a Rakefile in what would be the root of the puppet directory (i.e. it's at the same level as the modules directory). You can run the tests for all modules by running rake rspec from the project's root directory (again rspec is the default task, so just running rake will work too). You can run all the specs for a specific module by running rake rspec:[module] e.g. rake rspec:foo. Running rake help (comes from puppetlabs_spec_helper) will show the full list of module tasks.
Caveat 1: Running rake rspec is like cding into each module directory and running rake rspec, except that it isn't, so there might be some weird things to look out for!?
Caveat 2: If you run rake rspec and the task for one of the modules fails, no subsequent tasks in the list will run. Ideally the tasks for all the modules should run even if one (or all) of them fail. If someone could just fix it, that would be great :)
You can use this same setup to mock classes, defined types, and functions within specs for classes, defined types or functions
I'm new to rspec and rspec-puppet and still relatively new to ruby so there are probably nicer/better ways to do some of this stuff but it works for me so far.
Comments, questions and constructive criticism are all welcome!