8

有没有办法跟踪使用 let 时创建的变量?

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

当我完成该测试时,问题就出现了。我想使用 server.kill() 杀死服务器。如果我能说一些大意的话,这几乎是完美的

after(:each) { server.kill }

但这会创建服务器并在引用它时浪费所有资源/时间来创建它,只有在前面的测试中没有使用服务器时才会立即杀死它。有没有办法跟踪并仅在服务器已使用时才对其进行清理?

4

4 回答 4

4

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

describe MyTest do

  before(:each) { @created_server = false }

  let(:server) { 
    @created_server = true
    Server.new 
  }

  after(:each) { server.kill if @created_server }

end
于 2013-06-23T10:27:26.827 回答
3

我会做的是这样的:

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
于 2013-06-06T20:21:14.587 回答
2

这绝对是一个黑客:

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 自己看到了它的好处,那么可以在核心中完成一些事情......

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

于 2013-08-19T08:14:21.873 回答
0

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

想象这是需要启动的真实服务器:

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 块显式启动服务器,这在输出中也可以看到。

于 2016-01-31T14:13:35.170 回答