如何在测试中Stub Thor CLI的location方法并消除警告?
问题描述
我定义了一个继承自Thor的CLI类,其中location getter方法被放在no_commands块中:
# 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
我正在为该类编写单元测试,验证命令是否正确委托给location实例:
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
但CLI始终调用实际的location方法而非Stub后的方法,我猜测这是因为Thor会创建新实例,不会使用当前的cli实例。于是我尝试将allow(cli)替换为:
allow_any_instance_of(CLI).to receive(:location).and_return location
这能正常工作,但控制台会出现警告:
[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'".
请问如何消除这个警告?我试过直接用CLI.no_commands { allow_any_instance_of ... }但无效。
解决方法
方法1:绕过allow_any_instance_of,Stub依赖链上游
不要直接StubCLI#location,而是StubLocation类的detect和new方法,这样无论Thor创建多少个CLI实例,都会返回你预设的mock对象:
describe CLI do let(:location) { instance_double(Location) } let(:detected_path) { '/fake/test/path' } before :each do # Stub Location.detect 返回预设路径 allow(Location).to receive(:detect).with(Dir.pwd).and_return(detected_path) # Stub Location.new 返回mock的location实例 allow(Location).to receive(:new) .with(detected_path, File.join(detected_path, '.templates')) .and_return(location) end describe 'playground new NAME' do it 'passes the specified name to location' do described_class.invoke(:new_playground, ['example_playground']) expect(location).to have_received(:new_playground).with('example_playground') end end end
这种方式遵循测试最佳实践,无需使用侵入性强的allow_any_instance_of,同时避开了Thor的命令检测逻辑,不会触发警告。
方法2:为CLI添加依赖注入接口
修改CLI类,在no_commands块中添加一个location=方法,用于测试时注入mock对象:
# 修改后的CLI类 class CLI < Thor def self.exit_on_failure? true end no_commands do def location @location ||= begin path = Location.detect Dir.pwd throw 'Could not find a playgrounds directory' if path.nil? Location.new path, File.join(path, '.templates') end end # 测试专用的设置方法,不会被识别为Thor命令 def location=(loc) @location = loc end end end
然后在测试中拦截CLI的实例创建过程,注入mock:
describe CLI do let(:location) { instance_double(Location) } before :each do # 拦截new方法,创建实例后注入mock的location allow(CLI).to receive(:new).and_wrap_original do |original_method, *args| cli_instance = original_method.call(*args) cli_instance.location = location cli_instance end 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).with('example_playground') end end end
这种方式让测试逻辑更清晰,同时因为location=方法在no_commands块中,不会被Thor识别为命令,自然不会触发警告。
内容的提问来源于stack exchange,提问作者BadIdeaException
相关产品推荐
相关产品推荐

