2

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.

asked Sep 5, 2025 at 16:54
1
  • Maybe try using a class double with as_stubbed_constant? Another issue might be (untested) that no_commands appears to define the methods in a NestedContext Object. Source. I did not dig any deeper and unfortunately I am not in a position to test at the moment. Adding an example of the :new_playground command might be a useful addition to the post so that true testing is possible Commented Sep 5, 2025 at 20:54

1 Answer 1

1

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.

answered Sep 6, 2025 at 13:34
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.