0
votes

I have code like this:

module Empiric
  module_function

  def gem_version(name)
    Gem.loaded_specs[name].version
  rescue NoMethodError
    raise NoMethodError, "No gem loaded for #{name}"
  end
end

This is designed to look for a loaded gem and give the version, if the gem is found. In my project, the Capybara gem, for example, will NOT be there, so this works:

Empiric.gem_version("capybara")

When I say it "works", I mean it raises the appropriate exception. I'm trying to create an RSpec test for that and I came up with this:

it "indicates if a dependent gem is not found" do
  expect {
    Empiric.get_version("capybara")
  }.to raise_error NoMethodError
end

The test passes ... however, test coverage indicates my raise portion is not being covered.

Yet, if I change the above test to instead say .not_to raise_error NoMethodError, the test fails. As it should. Note I changed it to "not_to" instead of "to". So this shows me the test is, in fact, recognizing that an error was generated.

Does this have to do with the fact that I'm specifically rescuing the error? Because the rescue line shows that it's covered in the test coverage report, but the raise statement is not, even though I've verified that it does in fact occur.

In terms of trying to answer my own question, I did come across this (How to write down the rspec to test rescue block.?) but that solution didn't seem to work at all for me. I also looked at this (How do I test the rescue block of a method with rspec mocks 3.3), which also offers little in the way of help for me. Likewise for this (Rspec: testing a rescue) and this (Using RSpec how can I test the results of a rescue exception block) and this (Rspec: Test rescue block).

Just to be clear, I'm not operating in a Rails context. This is a standalone gem I'm writing.

1
In the first step, I'd check what message is attached to the exception with expect { ... }.to raise_error NoMethodError, "No gem loaded for capybara". Maybe it's a bug in coverage reporter?katafrakt
Interesting. When I try that I get this from rspec: expected NoMethodError with "No gem loaded for capybara", got #<NoMethodError: undefined method get_version' for Empiric:Module>`. Which is definitely odd, since I can call that method outside of a test without issue and without adding the exception message, I don't get this error about the method not being found at all. The coverage tool I'm using is SimpleCov.Jeff Nyman
Heh, it's get_version vs gem_version ;)katafrakt
Rescuing NoMethodError is not a good idea as its a really good way to mask bugs. Instead you should use Hash#fetch and rescue KeyError or the safe operator .&.max
Use Gem.loaded_specs.fetch(name).version and rescue KeyError or the safe operator Gem.loaded_specs.fetch(name).&version and check for nil.max

1 Answers

1
votes

Okay, so the answer here was extremely simple (thanks to @katafrakt for pointing it out). I'll put an answer here because I think it's interesting.

The problem is in my test:

it "indicates if a dependent gem is not found" do
  expect {
    Empiric.get_version("capybara")
  }.to raise_error NoMethodError
end

Notice I'm calling get_version rather than gem_version. But ... then why didn't the test indicate this? At least by failing, which I would have expected.

The only way I found the issue was by adding in the message that the exception provides. To wit:

...
.to raise_error NoMethodError, "No gem loaded for capybara."

That execution then told me there was no get_version method defined. Specifically I got this:

expected NoMethodError with "No gem loaded for capybara",
got #<NoMethodError: undefined method `get_version' for Empiric:Module>

Obviously fixing the test to call the correct method did the trick. But what's less clear is why the test was originally indicating as passing when I was clearly calling an incorrect method. Not only an incorrect method, but a non-existent one. I have no get_version method in my code at all.

But notice I'm rescuing a "NoMethodError" in the first place. So maybe that's the case here? Maybe the fact of get_version itself being a NoMethodError was getting swallowed up by the actions of the test? It was only when I put in a specific message for my exception did that then force RSpec to realize that the actual NoMethodError -- with a different message -- was occurring.

I was tempted to delete the question since I clearly made a bone-headed mistake. But I feel the context of that mistake might be interesting enough for others looking at this. If moderators disagree and feel this should be deleted, no worries. Otherwise, my stupidity can stay enshrined for all to see.