I have a Thor class that has a getter method defined like this:
# Playgrounds CLI skeleton class.
# Commands are added from commands folder
class CLI < Thor
def self.exit_on_failure?
true
end
no_commands do
def location
unless @location
path = Location.detect Dir.pwd
throw 'Could not find a playgrounds directory' if path.nil?
@location = Location.new path, File.join(path, '.templates')
end
@location
end
end
end
I am now trying to write a unit test for this class that checks that the wiring is correct (commands correctly hand off to the location
instance).
require_relative '../lib/cli'
require_relative '../lib/location'
describe CLI do
subject (:cli) { described_class.new }
let (:location) { instance_double(Location) }
before :each do
allow(Location).to receive(:detect).and_return('')
allow(cli).to receive(:location).and_return(location)
end
describe 'playground new NAME' do
it 'uses the specified name' do
cli.invoke(:new_playground, ['example_playground'])
expect(location).to have_received(:new_playground)
end
end
end
But the CLI always calls the actual location
method, not the stubbed-out one. I think this is because Thor actually creates a new instance, so it doesn't use cli
at all.
Based on this, I have tried to replace allow(cli)
with
allow_any_instance_of(CLI).to receive(:location).and_return location
This works, but it also produces this rather ugly warning on my console:
[WARNING] Attempted to create command "location" without usage or description. Call desc if you want this method to be available as command or declare it inside a no_commands{} block. Invoked from "/vagrant/vendor/bundle/ruby/3.4.0/gems/rspec-mocks-3.13.5/lib/rspec/mocks/any_instance/recorder.rb:223:in 'Module#alias_method'".
How can I wrap allow_any_instance_of
in a Thor no_commands
block so that it suppresses the warning? The obvious - CLI.no_commands { allow_any_instance_of ... }
doesn't work.
I was able to solve it by adding this:
before do
ctx = self
described_class.class_exec do
no_commands { ctx.allow_any_instance_of(self).to ctx.receive(:location).and_return ctx.location }
end
end
after do
described_class.class_exec do
no_commands { RSpec::Mocks.teardown }
end
end
Explanation, for posterity:
The key here is to execute the stubbing within the context of the described_class
, which allows me to run a no_commands
block. Inside that block, I can then do the actual stubbing. Since class_exec
changes self
, I need to preserve the old self
for access to the required methods.
This is almost enough to suppress the warnings, but not quite: After each test, RSpec automatically restores the old methods, and this causes another warning. Therefore, I added an after
block, where I do the same thing to manually restore the mocks.