Search code examples
ruby-on-railsrubyrspectwilio

How to fix Twilio::REST::RestError while testing with Rspec?


I'm new to testing with RSpec and don't know how to fix this error while test creating a new Customer where my CustomerController

class Api::V1::Customers::RegistrationsController < DeviseTokenAuth::RegistrationsController

  def create
        customer = Customer.new(email: params[:email],
                            password: params[:password],
                            password_confirmation: params[:password_confirmation],
                            first_name: params[:first_name],
                            last_name: params[:last_name],
                            telephone_number: params[:telephone_number],
                            mobile_phone_number: params[:mobile_phone_number])
        if customer.save
          customer.generate_verification_code
          customer.send_verification_code
          render json: {message: 'A verification code has been sent to your mobile. Please fill it in below.'}, status: :created
        else
          render json: customer.errors
        end
     end
  end
end

where generate_verification_code and send_verification_code in Customer

class Customer < ActiveRecord::Base

  def generate_verification_code
    self.verification_code = rand(0000..9999).to_s.rjust(4, "0")
    save
  end

  def send_verification_code
    client = Twilio::REST::Client.new
    client.messages.create(
      from: Rails.application.secrets.twilio_phone_number,
      to: customer.mobile_phone_number,
      body: "Your verification code is #{verification_code}"
    )
  end

end

and test file for registrations_controller_spec.rb for Customer

require 'rails_helper'

RSpec.describe Api::V1::Customers::RegistrationsController, type: :controller do
    let(:customer) { FactoryBot.create(:customer) }

    before :each do
        request.env['devise.mapping'] = Devise.mappings[:api_v1_customer]
    end

    describe "Post#create" do
        it 'creates a new customer' do
            post :create, params: attributes_for(:customer)
            expect(response).to have_http_status(:created)
        end
    end
end

I got this error, after running the test:

 Failure/Error:
   client.messages.create(
     from: Rails.application.secrets.twilio_phone_number,
     to: customer.mobile_phone_number
     body: "Your verification code is #{verification_code}"
   )

 Twilio::REST::RestError:
   Unable to create record: The requested resource /2010-04-01/Accounts//Messages.json was not found

I got it that this error is happening because in testing it shouldn't call external Api (when Twilio send sms verification code to number)!

But any ideas how to solve this ?


Solution

  • The reason why this is happening is that your tests are trying to hit twilio's API and probably don't have correct configuration for test environment. For testing third party calls like Twilio, you need to mock HTTP requests. Someone has suggested VCR in comments. However, in my opinion, you should mock the TwilioClient itself by creating a fake twilio adapter. Something like this –

    class TwilioAdapter
    
      attr_reader :client
    
      def initialize(client = Twilio::REST::Client.new)
        @client = client
      end
    
      def send_sms(body:, to:, from: Rails.application.secrets.twilio_phone_number)
        client.messages.create(
          from: from,
          to:   to,
          body: body,
        )
      end
    end
    

    Change send_verification_code method in Customer to –

    def send_verification_code
      client = TwilioAdapter.new
      client.send_sms(
        to: customer.mobile_phone_number,
        body: "Your verification code is #{verification_code}"
      )
    end
    

    Now you in before block of controller test, mock TwilioAdapter's send_sms method.

    require 'rails_helper'
    
    RSpec.describe Api::V1::Customers::RegistrationsController, type: :controller do
        let(:customer) { FactoryBot.create(:customer) }
    
        before :each do
          request.env['devise.mapping'] = Devise.mappings[:api_v1_customer]
          # Here's the expectation
          expect_any_instance_of(TwilioAdapter).to receive(:send_sms).with(hash_including(:body, :to))
        end
        …
    

    That should do the trick.

    In any case, I strongly discourage this pattern of making 3rd part calls synchronously via models. I recommend creating an SMS service with a generic interface that uses the above mention twilio adapter for sending sms and do this asynchronously using sidekiq. https://www.twilio.com/blog/2015/10/delay-api-calls-to-twilio-with-rails-active-job-and-sidekiq.html