You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在测试中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 09:33:18