【问题标题】:Rails test that method is called from the controllerRails 测试从控制器调用该方法
【发布时间】:2021-08-30 17:45:19
【问题描述】:

我有一个调用服务的控制器函数,我想测试是否使用正确的参数调用了该服务。

def send_for_signature
  client = Client.find(params[:client_id])
  external_documents = params[:document_ids].map{|id| ExternalDocument.find(id)}
  service = EsignGenieSendByTemplate.new(client: client, external_documents: external_documents, form_values: params[:form_values])
  result = service.process

  if result["result"] == "success"
    head 200
  else
    render json: result["error_description"], status: :unprocessable_entity
  end
end

如何编写测试以确保正确调用 EsignGenieSendByTemplate.new(client: client, external_documents: external_documents, form_values: params[:form_values])?

【问题讨论】:

  • 注意external_documents = params[:document_ids].map{|id| ExternalDocument.find(id)}可以写成external_documents = ExternalDocument.find(params[:document_ids]),以节省一些SQL查询

标签: ruby-on-rails rspec


【解决方案1】:

你需要的是一个叫做expecting messages的东西。

我通常会这样写:

it 'requests the signature' do
  expect(EsignGenieSendByTemplate).to receive(:new).with(client: 'A', external_documents: 'B', form_values: 'C')
  get :send_for_signature, params:  { ... }
  expect(response.status).to have_http_status(:success)
end

【讨论】:

    【解决方案2】:

    我将从向服务添加工厂方法开始:

    class EsignGenieSendByTemplate
      # ...
      def self.process(**kwargs)
        new(**kwargs).process
      end
    end
    

    这种代码是几乎所有类型服务对象的样板代码,并在服务对象与其消费者(如控制器)之间提供更好的 API。

    您的服务规范中的示例应涵盖此方法。

    describe '.process' do
      let(:options) do
        { client: 'A', external_documents: 'B', form_values: 'C' }
      end
    
      it "forwards its arguments" do
        expect(described_class).to recieve(:new).with(**options)
        EsignGenieSendByTemplate.process(**options)
      end
    
      it "calls process on the instance" do
        dbl = instance_double('EsignGenieSendByTemplate')
        allow(described_class).to recieve(:new).and_return(dbl) 
        expect(dbl).to recieve(:process)
        EsignGenieSendByTemplate.process(**options)
      end
    end
    

    您的控制器应该只调用工厂方法而不是实例化 EsignGenieSendByTemplate:

    def send_for_signature
      client = Client.find(params[:client_id])
      # Just pass an array to .find instead of looping - this create a single 
      # db query instead of n+1
      external_documents = ExternalDocument.find(params[:document_ids])
      result = EsignGenieSendByTemplate.process(
        client: client, 
        external_documents: external_documents, 
        form_values: params[:form_values]
      )
    
      if result["result"] == "success"
        head 200
      else
        render json: result["error_description"], status: :unprocessable_entity
      end
    end
    

    控制器和服务之间的这种更好的 API 让您可以对 EsignGenieSendByTemplate 类设置期望,这样您就不必胡乱使用 expect_any_instance 或存根 .new 方法。

    it 'requests the signature' do
      expect(EsignGenieSendByTemplate).to receive(:process).with(client: 'A', external_documents: 'B', form_values: 'C')
      get :send_for_signature, params:  { ... }
    end
    

    【讨论】:

    • 我非常感谢您的回复。几个问题,1) 我是否应该像在 "calls process on the instance" 测试中一样在 "forwards its arguments" 测试中定义 dbl?并且 2) 在以这种方式定义 dbl 之后,测试失败并声明:Failure/Error: new(**kwargs).process # received unexpected message :process with (no args)
    • 抱歉这是我的错误.and_return(dbl) 不应该出现在第一个示例中。已编辑。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-29
    • 1970-01-01
    • 1970-01-01
    • 2012-02-13
    相关资源
    最近更新 更多