<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://brokenco.de//feed/by_tag/rspec.xml" rel="self" type="application/atom+xml" /><link href="https://brokenco.de//" rel="alternate" type="text/html" /><updated>2026-05-03T00:12:50+00:00</updated><id>https://brokenco.de//feed/by_tag/rspec.xml</id><title type="html">rtyler</title><subtitle>a moderately technical blog</subtitle><author><name>R. Tyler Croy</name></author><entry><title type="html">Testing Puppet’s custom facts with RSpec</title><link href="https://brokenco.de//2014/03/01/testing-custom-facts-with-rspec.html" rel="alternate" type="text/html" title="Testing Puppet’s custom facts with RSpec" /><published>2014-03-01T00:00:00+00:00</published><updated>2014-03-01T00:00:00+00:00</updated><id>https://brokenco.de//2014/03/01/testing-custom-facts-with-rspec</id><content type="html" xml:base="https://brokenco.de//2014/03/01/testing-custom-facts-with-rspec.html"><![CDATA[<p>As a long-time user of both Puppet and Jenkins, it should not be terribly
surprising to readers that I’m creator of the most downloaded
<a href="http://forge.puppetlabs.com/rtyler/jenkins">puppet-jenkins</a> module on <a href="http://forge.puppetlabs.com">Puppet Forge</a>.</p>

<p>Maintaining a semi-popular Puppet module is an interesting endeavour. One must
try to maintain some semblance of quality, while still readily accepting
patches from numerous contributors who found a slightly different use-case than
your own. Since acceptance testing of Puppet modules on multiple architectures
is an enormous pain, there’s an inherent “running of the gauntlet” with every
release.</p>

<p>The first step to sanity was to start using
<a href="http://rspec-puppet.com">rspec-puppet</a>
with the module. Thanks to <a href="https://github.com/jeffmccune">Jeff McCune</a>, the
module has had rspec-puppet tests for the past <a href="https://github.com/jenkinsci/puppet-jenkins/commit/96e981da1cd94629e4b6e0dee3332af0fe24640d">two
years</a>.
Because Puppet’s DSL (domain-specific language) compiles a “catalog” (an
abstract syntax tree of resources), rspec-puppet allows for some level of
testing of your Puppet code, but it only can assure you that a catalog can be
correctly compiled.</p>

<p>For the purposes of the
<a href="https://github.com/jenkinsci/puppet-jenkins">puppet-jenkins</a> module, that
covers most  of the code in the module, but our <em><a href="http://docs.puppetlabs.com/guides/custom_facts.html">custom
facts</a></em> were left in the
dark.</p>

<h4 id="breakdown-of-a-fact">Breakdown of a Fact</h4>

<p>In its most basic form, a Fact is the named result of a block of Ruby code on
a machine which is executing that Ruby. The way this is accomplished, like most
things in Ruby projects, is via yet-another-DSL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># lib/facter/mymodule.rb
Facter.add("hello_world") do
  setcode do
    "i'm in your puppet!"
  end
end
</code></pre></div></div>

<p>The preceeding code will be auto-loaded by the Facter gem, and executed by the
Puppet agent, exposing <code class="language-plaintext highlighter-rouge">$hello_world</code> into your manifests with a value of <code class="language-plaintext highlighter-rouge">"i'm
in your puppet!"</code></p>

<p>Relatively simple, but there are a few problems:</p>

<ul>
  <li><em>Untestable</em>: This code invokes the Facter DSL at the Ruby file’s load
time, not on a specific execution-time invocation. This means if you were
to add <code class="language-plaintext highlighter-rouge">require 'lib/facter/mymodule.rb'</code> in an RSpec file, te code would run
well before your RSpec DSL is even invoked.</li>
  <li><em>Not very modular</em>: Since all our executable code is wrapped up inside the
DSL, re-using shared code is difficult, not impossible but hard to do in a
conventional Ruby style.</li>
  <li><em>Scripts, not software</em>: Most of the custom facts that I’ve seen rely on
<code class="language-plaintext highlighter-rouge">Facter::Util::Resolution#exec</code> for invoking shell commands, even where
there are existing Ruby modules and functions that provide the same information.
This isn’t inherently <em>bad</em>, but it does lead to treating custom facts more
like Ruby-styled shell scripts, rather than structured Ruby code.</li>
</ul>

<h4 id="restructuring-the-code-for-testability">Restructuring the code for testability</h4>

<p>In the case of the
<a href="https://github.com/jenkinsci/puppet-jenkins">puppet-jenkins</a> module’s sole
<code class="language-plaintext highlighter-rouge">$jenkins_plugin</code> Fact, this is what the code <a href="https://github.com/jenkinsci/puppet-jenkins/blob/d1bed59d18cac825f69d4779bd2a82858dfd7894/lib/facter/jenkins.rb">used to look
like</a>.
The goal of the fact is to express the installed plugins and their versions.
Unfortunately Jenkins plugins don’t have a version in their filename, but are
instead zip files containing Java bytecode, resources, and a single
<code class="language-plaintext highlighter-rouge">MANIFEST.MF</code> file, which contains all the necessary meta-data baout the
plugin.</p>

<p>Fortunately, Jenkins unzips this archive and sticks it on the file system when
a plugin is installed, so all the Fact needs to do is peek in a few
directories, simple right?</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if File.directory?(plugins)
  # Get a list of all plugins + versions
  Dir.entries(plugins).select do |plugin|
    if (File.directory?("#{plugins}/#{plugin}") == true) &amp;&amp; !(plugin == '..' || plugin == '.')
      begin
        contents = File.read("#{plugins}/#{plugin}/META-INF/MANIFEST.MF")
        contents =~ (/Plugin\-Version:\s+([\d\.\-]+)/)
        version = $1
        jenkins_plugins = "#{plugin} #{version}, " + jenkins_plugins
      rescue
        # Nothing really to do about it, failing means no version which will
        # result in a new plugin if needed
      end
    end
  end
end
</code></pre></div></div>

<p>Not only was this code not covered by RSpec, it does so many things, I had to
run it a few times to make sure I fully understood how it would work!</p>

<p>Forget about Facter for a moment, this becomes a classic “restructure for
testability” problem, which was <a href="https://github.com/jenkinsci/puppet-jenkins/blob/2b475e4aac927f9abd336388a37872349b894f93/lib/facter/jenkins.rb#L9">solved in this
commit</a>.</p>

<p>The commit introduces:</p>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">Jenkins::Facter</code> module with:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">#jenkins_home</code> - resolves an absolute path to the Jenkins home directory</li>
      <li><code class="language-plaintext highlighter-rouge">#add_facts</code> - installs the Fact</li>
    </ul>
  </li>
  <li>The <code class="language-plaintext highlighter-rouge">Jenkins::Facter::Plugins</code> module with:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">#directory</code> - resolves an absolute path to the plugin directory</li>
      <li><code class="language-plaintext highlighter-rouge">#manifest_data</code> - parse a string formatted the way <code class="language-plaintext highlighter-rouge">MANIFEST.MF</code> files
are formatted, into a Ruby <code class="language-plaintext highlighter-rouge">Hash</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">#exists?</code> - determine whether the Jenkins plugin directory exists</li>
      <li><code class="language-plaintext highlighter-rouge">#plugins</code> - compile the list of plugins and versions</li>
    </ul>
  </li>
</ul>

<p>That’s a lot of methods, with a lot more comments and <em>test coverage</em>! Looking
over the code again, I see more opportunities for refactoring; probably because
it’s just Ruby code to me now.</p>

<h4 id="testing-the-fact">Testing the Fact</h4>

<p>Refactoring into more Ruby-like code allows for easier testing of the Ruby-code
that’s powering our Facts, but what about the Facts <em>themselves</em>?</p>

<p>Above I mentioned the <code class="language-plaintext highlighter-rouge">#add_facts</code> method, which is added to avoid adding Facts
when the file is loaded into RSpec. It’s relatively straight-forward, just wrap
the previous DSL code in a method.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># lib/facter/mymodule.rb
module MyModule
  def self.add_facts
    Facter.add("hello_world") do
      setcode do
        "i'm in your puppet!"
      end
    end
  end
end

MyModule.add_facts
</code></pre></div></div>

<p>You’ll notice that at the bottom of the file, <code class="language-plaintext highlighter-rouge">#add_facts</code> is still invoked.
This is to ensure that the Facts we’ve defined will still get properly loaded
by the Facter gem when Puppet runs.</p>

<p>We’re still not done testing this Fact though! Facter loads facts into a global
collection, which means there’s some special sauce that you need to add to your
RSpec tests to make sure that Facter is cleared and properly reloaded for every
example, e.g.:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>describe 'hello_world fact' do
  subject(:fact) { Facter.fact(:hello_world) }

  before :each do
    # Ensure we're populating Facter's internal collection with our Fact
    MyModule.add_facts
  end

  # A regular ol' RSpec example
  its(:value) { should eql("i'm in your puppet!") }

  after :each do
    # Make sure we're clearing out Facter every time
    Facter.clear
    Facter.clear_messages
  end
end
</code></pre></div></div>

<hr />

<p>That’s more or less all there is to starting to add RSpec-based test coverage
for the custom facts you are including in your Puppet modules. Once you’re
writing RSpec, it’s easy to stub and mock out as much as necessary to cover all
the variations or cases you might need for your Fact.</p>

<p>All said and done, figuring this out took about 2 hours and in the process of
crafting <a href="https://github.com/jenkinsci/puppet-jenkins/commit/2b475e4aac927f9abd336388a37872349b894f93">this
commit</a>
the size of our <code class="language-plaintext highlighter-rouge">.rb</code> file went from ~28 lines of untested Facter DSL to ~115
lines of Ruby with ~160 lines of accompanying RSpec.</p>

<p>Now with each subsequent release of the puppet-jenkins module, I can have a
very high level of confidence that our custom facts will continue to work
correctly without manually testing them in an actual Puppet environment.</p>

<p>That’s one less thing to worry about when running the gauntlet, hitting that “Upload Module”
button, and creating a release to Puppet Forge.</p>]]></content><author><name>R. Tyler Croy</name></author><category term="puppet" /><category term="rspec" /><category term="facter" /><summary type="html"><![CDATA[As a long-time user of both Puppet and Jenkins, it should not be terribly surprising to readers that I’m creator of the most downloaded puppet-jenkins module on Puppet Forge.]]></summary></entry></feed>