Computer stuff, Ruby

`hide_const`, the RSpec helper that I had never used before

Introduction

Alright, let's try a shorter article for a change.

I have been trying out proper test-driven development for a couple months now, and I have to admit that once you get the hang of it, it's pretty amazing. I haven't been monitoring my defect rate carefully, but I can swear that my code has become simpler, cleaner and less susceptible to breakage.

However, learning how to properly do TDD, I had to improve my mastery of the testing framework beyond what I was used to. In particular, I recently discovered a RSpec helper that I had never used before.

The setup

Here is the gist of the method I was trying to test-develop. It's an extension for a library that only makes sense when an optional dependency is also installed. So, I wanted a method to check whether the extension could be used or not. Let's begin by writing a first test.

describe Library::Extension do
  describe '.available?' do
    subject(:available?) { described_class.available? }

    it { is_expected.to be true }
  end
end

Because I have the dependency installed for development purposes, then the nominal test says that the extension should be available. Let's write the minimal code that passes this test.

module Library
  module Extension
    ##
    # Checks whether the extension can be used.
    #
    # @return [Boolean]
    def self.available?
      true
    end
  end
end

Neato. Now, let's move back to the tests.

The hook

The second test we'll want to write will check that Extension.available? returns false when the optional dependency is not available.

Usually, in Ruby, these kinds of checks rely on testing for the presence of a constant, usually the top-level module for the dependent library. In our example, let's call that top-level module Dependency, because I am very original.

Let's write a test for this.

describe Library::Extension do
  describe '.available?' do
    subject(:available?) { described_class.available? }

    it { is_expected.to be true }

    context 'when Dependency is not available' do
      # ???

      it { is_expected.to be false }
    end
  end
end

Here's the catch, how the hell am I supposed to simulate Dependency not being installed? I can't really uninstall for real, nor can I "unrequire" it.

A naive approach

I just so happen to know that top-level modules names are simply constants defined the Object module. So, technically, it is possible to "remove" the constant and put it back after the test. So here is what I did.

describe Library::Extension do
  describe '.available?' do
    subject(:available?) { described_class.available? }

    it { is_expected.to be true }

    context 'when Dependency is not available' do
      around do |example|
        dependency = Dependency
        Object.send(:remove_const, :Dependency)
        example.run
      ensure
        Object.const_set('Dependency', dependency)
      end

      it { is_expected.to be false }
    end
  end
end

That... works, but it is very ugly: it relies on pretty low-level knowledge about the inner workings of Ruby, and it only works on top-level modules. If I wanted to do this on constants inside of a namespace, I would have to do the same thing on the correct module instead of Object, which is pretty annoying.

RSpec (always) got you covered

If you know how to read, you have seen the title of this post and you know where this is going. If you don't know how to read, what are you doing here? Shoo!

As it turns out, RSpec has the perfect helper for this situation, <a href="https://www.rubydoc.info/gems/rspec-mocks/RSpec%2FMocks%2FExampleMethods:hide_const">hide_const</a>. The documentation puts it better than I ever could:

Hides the named constant with the given value. The constant will be undefined for the duration of the test.
Like method stubs, the constant will be restored to its original value when the example completes.

-- https://www.rubydoc.info/gems/rspec-mocks/RSpec%2FMocks%2FExampleMethods:hide_const

We can re-write our test using this new-found knowledge.

describe Library::Extension do
  describe '.available?' do
    subject(:available?) { described_class.available? }

    it { is_expected.to be true }

    context 'when Dependency is not available' do
      before do
        hide_const('Dependency')
      end

      it { is_expected.to be false }
    end
  end
end

How terse! How clean! RSpec truly never ceases to amaze me. As a short conclusion, we can now go to our code and update .available? to pass the test.

module Library
  module Extension
    ##
    # Checks whether the extension can be used.
    #
    # @return [Boolean]
    def self.available?
      defined?(Dependency)
    end
  end
end

Conclusion

What's very interesting to me is that this helper is mentioned in RSpec's documentation, but the examples they provide are very basic and do not show real-world use-cases for most of the features.

Even though I'm sure it won't come up very often, I'm happy to have learned about this helper, and it goes to show that RSpec continues to prove itself as the greatest testing framework to ever grace my sight.

Post-scriptum

What?! Two posts in the same year?! What is going on? Back when I booted up this whole thing, I wrote the following.

I ultimately failed at my previous blog because I was putting way too much pressure on myself about the quality of my posts, their thoroughness...

-- https://blog.richarddegenne.fr/2022/08/07/hello-world/

So I'm finally trying out shorter posts. They're probably not as interesting as the longer ones, but they also require dramatically less time to write-up and with a months-old baby around, I'm not going to have that time anytime soon.

I still hope that was somewhat interesting and/or fun to read! Feel free to let me know what you think of this shorter format, and I'll see you around!

A question, a feedback? Write a comment!