don't worry, it's probably fine

Avoiding Duplication in Puppet Tests

10 Jan 2016

testing puppet

First post of 2016! It’s just a short one, sharing some observations made during the last week.

One of the core activities when test-driving code like you mean it is refactoring out duplication. Infrastructure-as-Code tooling for TDD exists but my experience of testing Puppet code (using rspec-puppet) is you end up with a lot of duplication between manifests and tests, and actually end up copying most of the manifest in the test.

This particularly turns up in Puppet’s exec blocks. When your manifest looks like …

class munge_log_files {

  exec { 'munge-log-files':
    command => 'find /path/to/files -name "*.log" | xargs cat > /tmp/some-munged-file.log',
    # ...
}

… your test ends up looking like this …

describe 'munge_log_files' do

  it { should compile }

  it do
    should contain_exec('munge-log-files')
     .with(:command => 'find /path/to/files -name "*.log" | xargs cat > /tmp/some-munged-file.log')
  end

end

The test is a near-duplicate of the manifest, only with syntax changes! The duplication is a result of not separating the existence of the subject from its content.

Taking advantage of .file() for refactoring

Puppet has built-in functions for extracting and manipulating data from the environment and available resources. The file function reads a file available in the Puppetmaster and assigns its return value - enabling the refactoring of data out from the declaration.

The test will stay the same, but the manifest looks like …

class munge_log_files {

  exec { 'munge-log-files':
    command => file('munge_log_files/command.sh'),
  }
}

… where the command is refactored out into modules/munge_log_files/files/command.sh.

Not only has this simplified the manifest but if that command needs to change in the future, the test will have to changed to reflect it but the manifest no longer needs to - there is an effective separation between code and data.

Other alternatives

Another option is to provision the whole script onto the machine as a file, and the exec will call out to that. As a matter of taste I try to avoid provisioning scripts out as files, especially if they’re one-liners for exec or cron tasks.