Writings on various topics (mostly technical) from Oliver Hookins and Angela Collins. We currently reside in Sydney after almost a decade in Berlin, have three kids, and have far too little time to really justify having a blog.
I've previously made presentations and blog posts about Puppet and module testing - my position is that you should treat Puppet code as just that: code. Just like mainstream programming languages, it is possible (and good practice) to test your Puppet manifests so that you have higher confidence in them working when it comes time to actually run them.
There are some other factors which play a part:
It is issues like this last point that seem to cause the most grief when testing Puppet modules in our environment. We have a collection of common modules called dist, which provide both re-usable functionality when required by application modules (e.g. the ability to easily set up a MySQL server, a standard way to provision Apache/Nginx etc) and configuration we expect to be standardized across all machines - in other words, the platform. In fact the wrapper class that pulls in the standardized configuration is called just that - "platform".
Platform pulls in a lot of helper functionality which application
modules can take for granted. An example is the yum class. Here we set
up a standard /etc/yum.conf with some tunable values, the
/etc/yum.repos.d fragments directory and a bunch of standard
repository fragments such as OS, Updates and so on. The module also
includes a defined type, yum::repo which acts much like the built-in
version but works with the yum class to have a fully managed fragments
directory - if a yum fragment is on disk but not managed by Puppet, it
is removed.
Naturally, in developing an application module you will want to set up a repository fragment to point to wherever you have your app packages stored, so all application modules utilise yum::repo at least once. Now, in testing your application class foo as a unit test, you might have the following code: ``
class foo {
yum::repo { 'myapp':
descr => 'Repository for My App',
...
}
...
}
To test it, you'd have the following in the tests directory: ``
class { 'foo': }
This is, of course, a trivial example with no parameters. Here, we already have a problem when attempting to unit test the class - it will immediately fail due to the yum class not having been instantiated in the catalog, thus not satisfying the dependency the yum::repo defined type has on it. Typically we have worked around this by just adding it to the test: ``
class { 'yum': }
class { 'foo': }
This is fine if you know which class you need to pull in, but if there are dependencies between resources in different classes it may not be so clear. The dependent resource will know what it needs, but not where it should retrieve it from. This pattern actually breaks encapsulation, so while it is acceptable in Puppet standard practice it is not very good practice from a developer standpoint.
Another idea we toyed with was automatically including a "stubbed out" version of the platform in every test, thus satisfying all dependencies that application classes may have without needing the user to specify them. I don't like this idea for a couple of main reasons:
In traditional unit-testing with external dependencies that we don't want to test, we would mock those dependencies. Good mocking libraries will also allow us to be explicit about call ordering, inputs and outputs expected in order to verify behaviour of our own code as well as the relationship it establishes with the external dependencies. Is this possible with Puppet? What would it look like? ``
define yum::repo (
$descr,
$baseurl,
$enabled,
...
) { }
class { 'foo': }
Now we have a somewhat mocked version of yum::repo that our class can use without having to worry about other chained dependencies outside of its view of the world. This starts introducing some other problems though:
I haven't spent us much time on this as on other Puppet problems previously, because at the moment (fortunately) it is mostly no more than an annoyance. We can add in the missing test dependencies by hand, and most of our users are becoming savvy enough to do it themselves. I'm interesting in what the community thinks about this topic though, and if you have solved this problem yourself? Please leave comments; I would love to know what you think!