Jeremy Thomas
Jeremy Thomas

Reputation: 6674

Rails test that method is called from the controller

I have a controller function that calls a service and I want to test that the service is called with the right arguments.

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

How would I write my test to ensure that EsignGenieSendByTemplate.new(client: client, external_documents: external_documents, form_values: params[:form_values]) is called correctly?

Upvotes: 0

Views: 1050

Answers (2)

max
max

Reputation: 101811

I would start by adding a factory method to the service:

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

This kind of code is boilerplate in almost any kind of service object and provides a better API between the service object and its consumers (like the controller).

This method should be covered by an example in your service spec.

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

Your controller should just call the factory method instead of instanciating 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

This better API between the controller and service lets you set an expectation on the EsignGenieSendByTemplate class instead so you don't have to monkey around with expect_any_instance or stubbing the .new method.

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

Upvotes: 1

razvans
razvans

Reputation: 3251

What you need is something called expecting messages.

I usually write something like this:

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

Upvotes: 1

Related Questions