如何使用rspec测试CLI的stdin

时间:2012-10-01 12:41:40

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

我正在制作一个小型Ruby程序,并且无法弄清楚如何编写模拟多个用户命令行输入的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

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

规格/ 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采用以下形式:

支持/ utilities.rb

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

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

规格/ 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),并且通常会重新安排此代码,但似乎无法获取任何存根以将数据放入stdin。我不知道我是否能够以与命令文件相同的方式解析命令行所期望的输入集,或者我应该用什么代码结构来解决这个问题。如果指定了命令行应用程序的人可以插入,那就太棒了。感谢。

4 个答案:

答案 0 :(得分:7)

我认为一些间接可以帮助你为这段代码编写单元测试,而不是对宇宙进行存根。您可以做的最简单的事情是允许注入输出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 *运算符)来克服主要障碍,然后将其传递给{ {1}}命令完成。如此简单,却如此难以捉摸。非常感谢this StackOverflow question and answer让我超越界限。

以下是代码:

"EXIT"

答案 2 :(得分:1)

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

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

答案 3 :(得分:0)

您可以使用Rspec&#39; s Thor::Actions

存根所有allow_any_instance_of

这是一个例子:

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