I have a fairly basic puppet module for a webservice running tomcat. I want to setup logrotate on Tomcat's catalina.out
file, and I want to start by writing a test that confirms logrotate is included in the module and setup with the correct settings.
Here's a stripped down version of my webservice.pp
, for example:
class my_module::webservice (
...
){
include ::tomcat_server
...
logrotate::rule { 'tomcat':
path => '/var/log/tomcat/catalina.out',
rotate => 1,
rotate_every => 'day',
copytruncate => true,
missingok => true,
compress => true,
delaycompress => true,
}
}
and I have included the logrotate forge module in my .fixtures.yml
like so:
fixtures:
forge_modules:
logrotate:
repo: 'puppet-logrotate'
ref: '3.2.1'
...
But I can only write a test that confirms that logrotate
is included in the module like so:
require 'spec_helper'
describe 'my_module::webservice' do
on_supported_os.each do |os, os_facts|
context "on #{os}" do
let(:facts) { os_facts }
it { is_expected.to compile }
it { is_expected.to contain_class('logrotate') }
end
end
end
This doesn't work (if I remove the logrotate block from init.pp
then the tests still pass):
it { is_expected.to contain_class('logrotate::conf') }
nor does asking for with
:
it { is_expected.to contain_class('logrotate') \
.with('path' => '/var/log/tomcat/catalina.out',
'rotate' => 1,
'rotate_every' => 'day',
'copytruncate' => true,
'missingok' => true,
'compress' => true,
'delaycompress' => true,
)
}
and nor does a separate/nested describe
block:
describe 'logrotate::rule' do
let(:title) { 'tomcat' }
let(:params) do
{
'path' => '/var/log/tomcat/catalina.out',
'rotate' => 1,
'rotate_every' => 'day',
'copytruncate' => true,
'missingok' => true,
'compress' => true,
'delaycompress' => true,
}
end
end
I can't find anything in the rspec docs that mention anything other than testing the class is defined. Is it even possible to do what I am trying to do?
Here is my directory layout:
puppet
`- modules
`- my_module
|- data
|- manifests
| |- init.pp
| `- webservice.pp
|- spec
| |- classes
| | `- webservice_spec.rb
| `- spec_helper.rb
|- .fixtures.yml
|- Gemfile
|- hiera.yaml
|- metadata.json
`- Rakefile
I have a fairly basic puppet module for a webservice running tomcat. I want to setup logrotate on Tomcat's catalina.out file, and I want to start by writing a test that confirms logrotate is included in the module and setup with the correct settings.
That sounds very reasonable. However, this ...
Here's a stripped down version of my init.pp, for example:
class my_module::webservice ( ... ){
... is at best poor practice. If it exists at all then the init.pp
manifest of module my_module
should define only class my_module
. A class named my_module::webservice
should instead be defined in a manifest named webservice.pp
in module my_module
. The expectations for module layout are documented in the Puppet online documentation. Although you might be able to get away with certain discrepancies from those specifications, there is only downside to doing so.
At this point I observe that "inner class" is not idiomatic Puppet terminology, and it suggests a misunderstanding of what you're working with. Specifically, this ...
logrotate::rule { 'tomcat':
[...]
... does not declare a class at all, but rather declares a resource of type logrotate::rule
, which is apparently a defined type provided by the puppet/logrotate module. In general, declaring a resource does not imply anything about classes from the module (if any) that provides the resource's type.
Furthermore, although it is entirely possible that declaring a logrotate::rule
resource does cause class logrotate
to be included in the catalog too, that would be an implementation detail of logrotate::rule
, and as such, your spec tests should not be testing for it. Only if my_module::webservice
is expected to itself declare class logrotate
should its tests be checking for that.
You go on to say:
This doesn't work (if I remove the logrotate block from init.pp then the tests still pass):
it { is_expected.to contain_class('logrotate::conf') }
You haven't presented enough code for us to determine why the tests pass when that is included in them, but something is very strange if ever that expectation is satisfied. logrotate::conf
is also a defined (resource) type, not a class, so that expectation should never succeed. And following a theme I introduced above, if class my_module::webservice
does not declare any logrotate::conf
resource directly then its tests should not be checking for one.
nor does asking for with:
it { is_expected.to contain_class('logrotate') \ .with('path' => '/var/log/tomcat/catalina.out', 'rotate' => 1, 'rotate_every' => 'day', 'copytruncate' => true, 'missingok' => true, 'compress' => true, 'delaycompress' => true, ) }
Of course that doesn't succeed. It expresses an expectation of a declaration of class logrotate
, but what you've actually declared is a resource of type logrotate::rule
. Even if logrotate::rule
did declare logrotate
, one would not expect it to pass on its own parameter list.
and nor does a separate/nested describe block:
describe 'logrotate::rule' do
[...]
Again, that's not surprising. Such a describe
block tells RSpec that logrotate::rule
is the class under test. Not only is it not the class under test (that is of course my_module::webservice
), but, again, logrotate::rule
is not a class at all. RSpec can certainly test defined types, too, but that's not what you're after here.
To test whether a resource is declared by the class under test, one uses a predicate of the form contain_
type(
title)
, where any namespace separators (::
) in the type name are replaced by double underscores. For example:
it do
is_expected.to contain_logrotate__rule('tomcat')
end
It is permitted, but optional, to include one or more with
clauses to specify expectations of the declared parameters of the designated resource. Following what you appear to have been trying to do, then, maybe this would more fully express what you're looking for:
require 'spec_helper'
describe 'my_module::webservice' do
on_supported_os.each do |os, os_facts|
context "on #{os}" do
let(:facts) { os_facts }
it do
is_expected.to compile
is_expected.to contain_logrotate__rule('tomcat')
.with(
path: '/var/log/tomcat/catalina.out',
rotate: 1,
rotate_every: 'day',
copytruncate: true,
missingok: true,
compress: true,
delaycompress: true
)
end
end
end
end
Do note, by the way, that when you want to test multiple predicates against the same example, it is substantially more efficient to group them together in the same it
block, as demonstrated above, than to put each in its own it
block. As in, you will probably notice the difference in test running time even from combining just two it
blocks into one.
Additionally, my example above demonstrates coding style close to that required to avoid warnings from pdk validate
, which brings us to an additional point: it is always useful to verify that pdk validate
completes without errors or warnings before trying the unit tests. You will probably find that it is excessively picky about both Puppet and Ruby code style, but it will also pick up some issues that lead to mysterious test failures. Also, it runs much faster than the tests do, and it will pick up substantially all syntax errors in both Puppet and Ruby code. It's frustrating to have your tests take a long time to fail on account of a minor syntax error.