如何使用rspec测试CLI的标准输入(stdin)

4

我正在编写一个小的Ruby程序,但是无法想出如何编写RSpect测试来模拟多个用户命令行输入(功能本身可以正常工作)。我认为这个StackOverflow答案可能涵盖了我所在的最接近的领域,但不完全符合我的要求。我正在使用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),并且经常重新排列这段代码,但似乎无法使我的存根将数据放入stdin中。我不知道是否可以像处理命令文件一样解析我从命令行预期的输入集,或者应该使用什么代码结构来解决此问题。如果有人已经规范化了命令行应用程序,请发表评论,谢谢。

4个回答

8

与其将整个宇宙桩起来,我认为增加一些间接层帮助你编写单元测试更为有效。最简单的方法是允许注入用于输出的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。
无论哪种方式,使您的代码更加友好的测试都会受益。

非常感谢您提供的有趣解决方案(我从未想过这样做),但不幸的是,这次它对我没有起作用。最终我找到了一个相对接近从文件中读取命令的解决方案,很快就会发布。 - Paul Fioravanti

3
我找到了一个解决方案,我认为它非常接近执行来自文件的指令的代码。我克服了主要障碍,最终意识到我可以写cli.stub(:gets).and_return并将我想要执行的命令数组作为参数(通过星号*操作符),然后传递 "EXIT" 命令来结束。如此简单,却又一直难以捉摸。感谢这个StackOverflow问题和答案推动我成功。
以下是代码:
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'方法来自哪里? - gnoll110
我已经有一段时间没有看过这个了,但我相信它是免费提供的,具体来说,它来自于Thor的spec helper - Paul Fioravanti
是的,谢谢。我没想到要在thor的spec目录下查找。我在lib目录下找了,唉!不过事后我还是找到了这个有用的东西。 :) http://www.arailsdemo.com/posts/57 - gnoll110

1

你看过Aruba吗?它可以让你为命令行程序编写Cucumber功能测试。这样,你就可以定义CLI的输入。

你可以使用RSpec编写功能定义,所以这并不是完全新的东西。


我因为命令行应用程序引导程序Methodone使用Aruba而知道它,虽然它看起来很酷,但如果可能的话,我认为我更愿意使用RSpec来解决这个问题。谢谢你的回答。 - Paul Fioravanti
@PaulFioravanti 你可以很有效地使用 Aruba 和 RSpec。 - Michael Mior

0

你可以使用 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

在新代码中,使用allow_any_instance是不被鼓励的,如RSpec文档所述。 - Paul Fioravanti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接