在 RSpec 测试过程中抑制控制台输出

53

我正在测试一个会在控制台输出一些信息的类(使用puts、p或warnings等)。 我想知道在 RSpec 测试期间是否有任何能力抑制此输出?


1
从架构上来说,最好使用日志库或以某种方式封装打印语句,以便可以从某个全局配置中切换打印。使用 sed 可以轻松地将所有的 printp 等用 Your::Logger.warn 或其他内容进行搜索和替换。 - alexanderbird
9个回答

50

我通过将$stout重定向到文本文件中来压制我的类中的puts输出。这样,如果出于任何原因我需要查看输出结果,它就在那里,但它不会混淆我的测试结果。

#spec_helper.rb
RSpec.configure do |config|
  config.before(:all, &:silence_output)
  config.after(:all,  &:enable_output)
end

public
# Redirects stderr and stout to /dev/null.txt
def silence_output
  # Store the original stderr and stdout in order to restore them later
  @original_stderr = $stderr
  @original_stdout = $stdout

  # Redirect stderr and stdout
  $stderr = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
  $stdout = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
end

# Replace stderr and stdout so anything else is output correctly
def enable_output
  $stderr = @original_stderr
  $stdout = @original_stdout
  @original_stderr = nil
  @original_stdout = nil
end

编辑:

针对@MyronMarston的评论,更明智的做法可能是将方法直接插入beforeafter作为块。

#spec_helper.rb
RSpec.configure do |config|
  original_stderr = $stderr
  original_stdout = $stdout
  config.before(:all) do 
    # Redirect stderr and stdout
    $stderr = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
    $stdout = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
  end
  config.after(:all) do 
    $stderr = original_stderr
    $stdout = original_stdout
  end
end

它看起来更加清爽,并且避免了在main函数中使用方法。 此外,请注意,如果您正在使用Ruby 2.0,则可以使用__dir__代替File.dirname(__FILE__)

编辑2

还应该提到,您可以通过使用File::NULL将输出重定向到真正的/dev/null, 这是在Ruby v 1.9.3(jruby 1.7)中引入的。

然后代码段将如下所示:

#spec_helper.rb
RSpec.configure do |config|
  original_stderr = $stderr
  original_stdout = $stdout
  config.before(:all) do
    # Redirect stderr and stdout
    $stderr = File.open(File::NULL, "w")
    $stdout = File.open(File::NULL, "w")
  end
  config.after(:all) do
    $stderr = original_stderr
    $stdout = original_stdout
  end
end

4
这是一个相当不错的解决方案,但请注意它会向系统中的每个对象添加silence_outputenable_output。我认为在编写小型一次性脚本时这样做没问题,但除此之外,我避免在main上定义方法。 - Myron Marston
1
@MyronMarston 您是正确的。虽然它只是在 RSpec 测试中使用,因此可能不会引起问题,但最好将它们从 main 中排除。我更新了我的答案以反映这一点。 - Charles Caldwell
1
另一个提示:我在共享上下文“mute sterror and stout”中使用此重定向,而不是全局钩子,使用before(:each){}after(:each){}-在我的设置中(rspec 3.2),它抑制了示例的输出,同时保留了其他(在我的情况下更有用的)rspec输出。 - Ivan Kolmychek
1
我刚刚发现了 RSpec 的新功能 expect { <code with output> }.to output(..).to_stdout,你不再需要重定向输出,RSpec 实际上会捕获它并且不输出!请参见:https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/output-matcher - Arthur Maltson
1
请注意 如果您使用pry,我发现这个设置会吞噬pry输出到控制台。有没有解决方法? - Chris Hough
显示剩余6条评论

30

尝试为在before块中输出的方法进行存根处理,例如:

before do
  IO.any_instance.stub(:puts) # globally
  YourClass.any_instance.stub(:puts) # or for just one class
end

这是显式的,因此您不会错过任何您不想错过的内容。如果您不关心任何输出,并且上述方法不起作用,您始终可以存根IO对象本身:

before do
  $stdout.stub(:write) # and/or $stderr if needed
end

非常简单,只需在那些我知道会写入但不想要的测试中存根出$stdout,谢谢 :) - sevenseacat

29

一个 Rspec3.0 版本将会在 spec_helper.rb 中。

RSpec.configure do |c|
  c.before { allow($stdout).to receive(:puts) }
end

它将作为before(:each)的作用

但是:each是默认值,所以不需要显式地编写它


5
使用 :write 而不是 :puts 进行存根测试对我很有效。 - mc9
allow($stdout).to receive(:puts)(或:write)应该是被接受的答案。 - Yo Ludke

19

测试使用 rspec-core(约为 3.4.0)

在 describe 块中,您可以这样做:

# spec_helper.rb
def suppress_log_output
  allow(STDOUT).to receive(:puts) # this disables puts
  logger = double('Logger').as_null_object
  allow(Logger).to receive(:new).and_return(logger)
end

# some_class_spec.rb
RSpec.describe SomeClass do
  before do
    suppress_log_output
  end
end

这种方法可以为特定的测试切换日志输出,但请注意,不会抑制rspec警告或消息。

另一种禁用来自gem的警告的方法:

config.warnings = false 添加到spec_helper中

如果您想要抑制仅限于某些记录器方法(例如error、info或warn),则可以执行以下操作

allow_any_instance_of(Logger).to receive(:warn).and_return(nil)

要禁用来自rspec gem的警告,请执行以下操作

allow(RSpec::Support).to receive(:warning_notifier).and_return(nil)

但是,通常不建议这样做,因为它旨在让您知道在测试中存在问题。


10
如果您想抑制单个测试的输出,有一种更简洁的方法:

it "should do something with printing" do
  silence_stream(STDOUT) do
    foo.print.should be_true
  end
end

如果您的测试打印了错误信息,您可能希望将 STDOUT 更改为 STDERR


3
我想指出,silence_streamactivesupport 的一部分。 - Andrey Chernih
6
silence_stream现已弃用。 - dgmstuart

8

以下是针对Rails 5的更新答案,适用于单次操作:

before do
  RSpec::Mocks.with_temporary_scope do
    allow(STDOUT).to receive(:puts)
  end
end

如果您需要频繁执行此操作,可以将其转化为spec_helper中的一个方法。

7
在尝试了所有这些例子之后,我最终使用了此变体,它不会静音或消除binding.pry
# frozen_string_literal: true

RSpec.configure do |config|
  config.before(:each) do
    allow($stdout).to receive(:puts)
    allow($stdout).to receive(:write)
  end
end

4
这个帖子中,对我起作用的仅有这个解决方案。对我而言,只有 write 桩代码是必需的。 - Matt

0

你可以让对象根据环境自行抑制:

class Foo
  def call
    puts("blah blah")
    # ...
  end

  private
  
  def puts(msg)
    return if ENV['APP_ENV'] == 'test'
    super
  end
end

0

在编程中,注入一个默认为STDOUT的IO对象是有用的。这也使得如果需要在输出上进行断言更加容易。

例如:

def my_method(arg, io: STDOUT)
  io.puts "hello"
  arg.reverse
end

然后在你的测试中:

# Suppress it.

my_method("hi", io: StringIO.new)

# Assert on it.

io = StringIO.new
my_method("hi", io: io)

output = io.tap(&:rewind).read
expect(output).to include("hello")

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