Rspec let() cleanup



是否有一种方法来跟踪使用let时创建的变量?

我有一系列测试,其中一些使用let(:server) {#blah blah}。其中一部分是等待服务器启动,以便它在使用之前处于适当的状态。

当我完成测试时,问题出现了。我想使用server.kill()终止服务器。如果我能像

这样说,那就太完美了
after(:each) { server.kill }

但是这会创建服务器,并在引用它时浪费创建它的所有资源/时间,只有在服务器没有在前面的测试中使用时才会立即杀死它。是否有一种方法可以跟踪并仅在服务器已被使用时清理服务器?

我也遇到过类似的问题。解决这个问题的一个简单方法是在let方法中设置一个实例变量来跟踪对象是否被创建:

describe MyTest do
  before(:each) { @created_server = false }
  let(:server) { 
    @created_server = true
    Server.new 
  }
  after(:each) { server.kill if @created_server }
end

我要做的是:

describe MyTest do
  let(:server) { Server.new }
  context "without server" do
    ## dont kill the server in here.
  end
  context "with server" do
   before do
     server
   end
   after(:each) { server.kill }
   it {}
   it {}
  end
end

这绝对是一个hack:

describe "cleanup for let" do
  let(:expensive_object) {
    ExpensiveObject.new
  }
  after(:context) {
    v = __memoized[:expensive_object]
    v.close if v
  }
end

我认为rspec必须将这些惰性值存储在实例可以访问的地方,而__memoized就是那个地方。

使用帮助器,它变得更整洁:

def cleanup(name, &block)
  after(:context) do
    v = __memoized[name]
    instance_exec(v, &block) if v
  end
end
describe "cleanup for let" do
  let(:expensive_object) {
    ExpensiveObject.new
  }
  cleanup(:expensive_object) { |v|
    v.close
  }
end

但仍有改进的余地。我不希望再输入两次对象的名称,所以这样会更好:

describe "cleanup for let" do
  let(:expensive_object) {
    ExpensiveObject.new
  }.cleanup { |v|
    v.close
  }
end

我不确定如果不把rspec拆成碎片我能做到这一点,但也许如果rspec自己看到了它的好处,可以在core中做一些事情…

编辑:改为使用instance_exec,因为rspec开始抱怨,如果事情从错误的上下文中调用,并将清理更改为after(:context),因为显然这是它记忆的级别。

只需编写一个小装饰器来处理服务器的显式和隐式启动,并允许您确定服务器是否已启动。

假设这是需要启动的真实服务器:

class TheActualServer
  def initialize
    puts 'Server starting'
  end
  def operation1
    1
  end
  def operation2
    2
  end
  def kill
    puts 'Server stopped'
  end
end

可重用的装饰器可以像这样:

class ServiceWrapper < BasicObject
  def initialize(&start_procedure)
    @start_procedure = start_procedure
  end
  def started?
    !!@instance
  end
  def instance
    @instance ||= @start_procedure.call
  end
  alias start instance
  private
  def method_missing(method_name, *arguments)
    instance.public_send(method_name, *arguments)
  end
  def respond_to?(method_name)
    super || instance.respond_to?(method_name)
  end
end

现在你可以像这样在你的规格中应用它:

describe 'something' do
  let(:server) do
    ServiceWrapper.new { TheActualServer.new }
  end
  specify { expect(server.operation1).to eql 1 }
  specify { expect(server.operation2).to eql 2 }
  specify { expect(123).to be_a Numeric }
  context 'when server is running' do
    before(:each) { server.start }
    specify { expect('abc').to be_a String }
    specify { expect(/abc/).to be_a Regexp }
  end
  after(:each) { server.kill if server.started? }
end

当一个方法在装饰器上被调用时,如果存在的话,它将运行它自己的实现。例如,如果调用#started?,它将回答实际的服务器是否已经启动。如果它没有该方法的自己的实现,它将把该方法调用委托给由该方法返回的服务器对象。如果此时它没有对实际服务器实例的引用,它将运行提供的start_procedure来获取一个实例,并记住它以备将来调用。

如果你把所有发布的代码放到一个名为server_spec.rb的文件中,你可以运行它:

rspec server_spec.rb

输出如下所示:

something
Server starting
Server stopped
  should eql 1
Server starting
Server stopped
  should eql 2
  should be a kind of Numeric
  when server is running
Server starting
Server stopped
    should be a kind of String
Server starting
Server stopped
    should be a kind of Regexp
Finished in 0.00165 seconds (files took 0.07534 seconds to load)
5 examples, 0 failures

请注意,在示例1和2中,调用了服务器上的方法,因此您看到的是由装饰器隐式启动的服务器的输出。

在示例3中,根本没有与服务器交互,因此您不会在日志中看到服务器的输出。

然后在示例4和5中,示例代码中没有与服务器对象直接交互,但服务器通过before块显式启动,这也可以在输出中看到。

最新更新