【问题标题】:Rspec let() cleanupRspec let() 清理
【发布时间】:2013-06-06 20:11:58
【问题描述】:

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

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

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

after(:each) { server.kill }

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

【问题讨论】:

    标签: ruby rspec


    【解决方案1】:

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

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

    【讨论】:

    • 我喜欢这种方法。谢谢分享。我用它来拆解一些在测试期间添加到数据库中但在测试运行后没有被删除的模型。
    【解决方案2】:

    我会做这样的事情:

    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
    

    【讨论】:

    • 我可以做类似的事情,但不是让 let(:server) {} 位于顶级描述块中,而是将其移动到“with server”上下文,删除 before(:each ) {} 然后 after(:each} {} 会起作用...
    • @MichaelPapile: let 块在上下文中只被调用一次,所以 after 块应该可以正常工作。
    • @MichaelPapile:我之前遇到过 let() 的问题,需要存在一些 let() 的副作用,但还不存在,因为导致它的 let() 没有'尚未被引用。在我知道 let!() 之前,我使用了一种解决方法,其中我有一个 before(:each) 块,它只包含有问题的 let() 的名称,这意味着 let() 的上下文扩展到包括 before (:each) 块用于该测试,并且应该合理地包含 after(:each) 块。
    【解决方案3】:

    这绝对是一个黑客:

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

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-07-18
        • 1970-01-01
        • 2020-07-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-02-14
        • 1970-01-01
        相关资源
        最近更新 更多