Resetting stubs in Mocha

Back to Listing

Hanover, MD, 17 December 2017


In the course of developing and testing various components for working with Puppet, I had a particularly difficult time with stubbing calls to other Ruby objects outside of my control.

After a ridiculous amount of searching, I finally found the answer that made the whole process seamless.

In general, when testing, you really don’t want to stub out methods on objects that you don’t actually control. However, when you’re testing components that explicitly deal with system interactions, you’re going to run into numerous cases where this simply isn’t tenable.

The Problem

The code that I was wrangling with was a Puppet Fact called login_defs that simply read the contents of /etc/login.defs and converted it into a reasonable Hash for processing by other Ruby code.

As you may expect, I had the following call in my code

login_defs_content = File.read('/etc/login.defs')

The Test

The logical method for testing this is something like the following:

describe 'custom fact login_defs' do
  before(:each) do
    Facter.clear
  end

  context 'when reading /etc/login.defs' do
    let(:login_defs_content) { <<-EOM
        UID_MIN 1000
        UID_MAX 60000
      EOM
    }

    it 'should return hash values from /etc/login.defs' do
      File.expects(:read).with('/etc/login.defs').returns(login_defs_content)

      expect(Facter.fact('login_defs').value).to eq({
        'uid_min' => 1000,
        'uid_max' => 60000
      })
    end
  end
end

The Issue

Unfortunately, when the code above is executed, something else is almost guaranteed to call File.read at some point and, regardless of the with statement, you’ll end up with an Unexpected Invocation error from Mocha.

Honestly, while this does not work the way that I expects, it works how it works and there’s not much that I could find to do about the default behavior.

The Solution

Honestly, I stumbled across this solution somewhere online (If I find it again, I’ll cite it here), and realized that I really needed to get this recorded so that I could find it again in the future.

To make stubs work generally how you would expect, you need to have an associated statement similar to the following that essentially stubs all other matching calls and ignores them.

File.stubs(:read).with(Not(equals('/etc/login.defs')))

Now, this may have unintended side effects but, so long as your tests are well confined, there should be no issue in general.

The resulting code would looks something like the following:

describe 'custom fact login_defs' do
  before(:each) do
    Facter.clear
  end

  context 'when reading /etc/login.defs' do
    let(:login_defs_content) { <<-EOM
        UID_MIN 1000
        UID_MAX 60000
      EOM
    }

    it 'should return hash values from /etc/login.defs' do
      File.expects(:read).with('/etc/login.defs').returns(login_defs_content)

      File.stubs(:read).with(Not(equals('/etc/login_defs')))

      expect(Facter.fact('login_defs').value).to eq({
        'uid_min' => 1000,
        'uid_max' => 60000
      })
    end
  end
end

Trevor has worked in a variety of IT fields over the last decade, including systems engineering, operating system automation, security engineering, and various levels of development.

At OP his responsibilities include maintaining overall technical oversight for Onyx Point solutions, providing technical leadership and mentorship to the DevOps teams. He is also responsible for leading OP’s solutions and intellectual property development efforts, setting the technical focus of the company, and managing OP products and related services. In this regard, he oversees product development and delivery as well as developing the strategic roadmap for OP’s product line.

At Onyx Point, our engineers focus on Security, System Administration, Automation, Dataflow, and DevOps consulting for government and commercial clients. We offer professional services for Puppet, RedHat, SIMP, NiFi, GitLab, and the other solutions in place that keep your systems running securely and efficiently. We offer Open Source Software support and Engineering and Consulting services through GSA IT Schedule 70. As Open Source contributors and advocates, we encourage the use of FOSS products in Government as part of an overarching IT Efficiencies plan to reduce ongoing IT expenditures attributed to software licensing. Our support and contributions to Open Source, are just one of our many guiding principles

  • Customer First.
  • Security in All We Do.
  • Pursue Innovation with Integrity.
  • Communicate Openly and Respectfully.
  • Offer Your Talents, and Appreciate the Talents of Others

ruby, mocha, testing, stub

Share this story

We work with these Technologies + Partners

puppet
gitlab
simp
beaker
redhat
AFCEA
GitHub
FOSSFeb