【问题标题】:How to test stdin for a CLI using rspec如何使用 rspec 为 CLI 测试标准输入
【发布时间】:2012-10-01 12:41:40
【问题描述】:

我正在制作一个小型 Rub​​y 程序,但不知道如何编写模拟多个用户命令行输入的 RSpec 规范(该功能本身有效)。我认为this StackOverflow answer 可能覆盖了离我最近的地方,但这并不是我所需要的。我在命令行界面使用Thor,但我认为这不是 Thor 中的任何问题。

该程序可以从文件或命令行读取命令,并且我已经能够成功编写测试以读取并执行它们。这是一些代码:

cli.rb

class CLI < Thor
  # ...
  method_option :filename, aliases: ['-f'],
                desc: "name of the file containing instructions",
                banner: 'FILE'

  desc "execute commands", "takes actions as per commands"
  def execute
    thing = Thing.new
    instruction_set do |instructions|
      instructions.each do |instruction|
        command, args = parse_instruction(instruction) # private helper method
        if valid_command?(command, args) # private helper method
          response = thing.send(command, *args)
          puts format(response) if response
        end
      end
    end
  end

  # ...

  no_tasks do
    def instruction_set
      if options[:filename]
        yield File.readlines(options[:filename]).map { |a| a.chomp }
      else
        puts usage
        print "> "
        while line = gets
          break if line =~ /EXIT/i
          yield [line]
          print "> "
        end
      end
    end
    # ..
  end

我已经成功测试了使用此代码执行文件中包含的命令:

spec/cli_spec.rb

describe CLI do

  let(:cli) { CLI.new }

  subject { cli }

  describe "executing instructions from a file" do
    let(:default_file) { "instructions.txt" }
    let(:output) { capture(:stdout) { cli.execute } }

    context "containing valid test data" do
      valid_test_data.each do |data|
        expected_output = data[:output]

        it "should parse the file contents and output a result" do
          cli.stub(:options) { { filename: default_file } } # Thor options hash
          File.stub(:readlines).with(default_file) do
            StringIO.new(data[:input]).map { |a| a.strip.chomp }
          end
          output.should == expected_output
        end
      end
    end
  end
  # ...
end

而上面提到的valid_test_data的格式如下:

support/utilities.rb

def valid_test_data
  [
    {
      input: "C1 ARGS\r
              C2\r
              C3\r
              C4",
      output: "OUTPUT\n"
    }
    # ...
  ]
end

我现在想做的是完全相同的事情,但我不想从“文件”中读取每个命令并执行它,而是想以某种方式模拟用户输入stdin。下面的代码完全错误,但我希望它能传达我想要的方向。

spec/cli_spec.rb

# ...
# !!This code is wrong and doesn't work and needs rewriting!!
describe "executing instructions from the command line" do
  let(:output) { capture(:stdout) { cli.execute } }

  context "with valid commands" do
    valid_test_data.each do |data|
      let(:expected_output) { data[:output] }
      let(:commands) { StringIO.new(data[:input]).map { |a| a.strip } }

      it "should process the commands and output the results" do
        commands.each do |command|
          cli.stub!(:gets) { command }
          if command == :report
            STDOUT.should_receive(:puts).with(expected_output)
          else
            STDOUT.should_receive(:puts).with("> ")
          end
        end
        output.should include(expected_output)
      end
    end
  end
end

我已经尝试在各个地方使用cli.stub(:puts),并且通常会重新排列这段代码,但似乎无法让我的任何存根将数据放入标准输入。我不知道我是否可以像处理命令文件一样解析我期望从命令行获得的输入集,或者我应该使用什么代码结构来解决这个问题。如果已经指定命令行应用程序的人可以加入进来,那就太好了。谢谢。

【问题讨论】:

    标签: ruby rspec command-line-interface stdin thor


    【解决方案1】:

    我认为一些间接操作可以帮助您为此代码编写单元测试,而不是对整个宇宙进行存根。您可以做的最简单的事情是允许注入输出的IO 对象,并将其默认为STDOUT

    class CLI < Thor
      def initialize(stdout=STDOUT)
        @stdout = stdout
      end
    
      # ...
    
      @stdout.puts # instead of raw puts
    end
    

    然后,在您的测试中,您可以使用StringIO 来检查打印的内容:

    let(:stdout) { StringIO.new }
    let(:cli) { CLI.new(stdout) }
    

    另一种选择是使用 Aruba 或类似的东西,并编写完整的集成测试,您可以在其中实际运行您的程序。这也有其他挑战(例如非破坏性并确保不压缩/使用系统或用户文件),但会更好地测试您的应用。

    Aruba 是 Cucumber,但断言可以使用 RSPec 匹配器。或者,您可以使用 Aruba 的 Ruby API(未记录,但公开且效果很好)来执行此操作,而无需 Gherkin 的麻烦。

    无论哪种方式,您都将受益于使您的代码更易于测试。

    【讨论】:

    • +1 为您提供有趣的解决方案(从未想过这样做),但不幸的是,这次它对我不起作用。我最终找到了一个与从文件中读取命令的解决方案相对接近的解决方案,我将很快发布。
    【解决方案2】:

    我最终找到了一个解决方案,我认为该解决方案与用于从文件执行指令的代码非常接近。我克服了主要障碍,最终意识到我可以编写 cli.stub(:gets).and_return 并将其传递到我想要执行的命令数组中(由于 splat * 运算符作为参数),然后将 "EXIT" 命令传递给结束。如此简单,却又如此难以捉摸。非常感谢this StackOverflow question and answer 将我推到了终点。

    代码如下:

    describe "executing instructions from the command line" do
      let(:output) { capture(:stdout) { cli.execute } }
    
      context "with valid commands" do
        valid_test_data.each do |data|
          let(:expected_output) { data[:output] }
          let(:commands) { StringIO.new(data[:input]).map { |a| a.strip } }
    
          it "should process the commands and output the results" do
            cli.stub(:gets).and_return(*commands, "EXIT")
            output.should include(expected_output)
          end
        end
      end
      # ...
    end
    

    【讨论】:

    • 嗨。我错过了一些东西。请问'capture'方法是从哪里来的。
    • 我已经有一段时间没有看到这个了,但我相信 Thor 是免费提供的:具体来说,from its spec helper
    • 好的,谢谢。没想到去看thor的spec目录。在 lib 中查看,doh!事后确实发现这很有帮助。 :) arailsdemo.com/posts/57
    【解决方案3】:

    你看过Aruba吗?它允许您为命令行程序编写 Cucumber 功能测试。您可以通过这种方式定义 CLI 的输入。

    您可以使用 RSpec 编写功能定义,因此它不是全新的。

    【讨论】:

    • 我知道 Aruba 是因为命令行应用程序引导程序 Methodone 使用它,虽然它看起来很酷,但如果可能的话,我想我更喜欢使用 RSpec 来解决这个问题。谢谢你的回答。
    • @PaulFioravanti 您可以非常有效地将 Aruba 与 RSpec 结合使用。
    【解决方案4】:

    您可以使用 Rspec 的 allow_any_instance_of 存根所有 Thor::Actions

    这是一个例子:

    it "should import list" do
       allow_any_instance_of(Thor::Actions).to receive(:yes?).and_return(true)
    end
    

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-06-09
    • 2021-03-21
    • 2011-09-14
    • 2012-12-24
    • 1970-01-01
    • 2014-08-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多