rubyrspecpuppetpdk

Test an inner class exists with RSpec and PDK


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

Solution

  • 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.