Design a site like this with WordPress.com
Get started

Simulando la llamada a un servicio externo con RSpec

Cuando realizamos pruebas unitarias, es común que necesitemos simular la llamada a un servicio externo. Por ejemplo, si nuestro sistema se comunica con una API, una gema o un Rails Engine, y queremos probar que nuestro “lado” funciona correctamente, necesitaremos simular la llamada a ese servicio externo.

Si no realizamos esta simulación, estaremos haciendo al test dependiente de este servicio externo, una mala práctica que, de hecho, por defecto RSpec prohíbe.

Existen varias herramientas disponibles y efectivas para simular o “doblar” objetos de test: Stubs, mocks, dobles… Un tema muy extenso sobre el que se arroja luz en este maravilloso articulo de @womanonrails.

Hoy vamos a ver un ejemplo concreto de uso de dobles.

Caso de uso

Imaginemos una clase UserManagementInternalService, dentro de la cual hemos encapsulado la llamada a la gema UserManagementExternalService que es la que gestiona la actualización de los datos de usuario:

class UserManagementInternalService
  def initialize(user:)
    @user = user
  end

  def update_user(new_details:)
    response = UserManagementExternalService::Client
      .new
      .update_user(
        user_id: @user.id,
        user_details: new_details
      )
    
    {
      updated_details: response[:user],
      errors: response[:errors]
    }
  end

En nuestro test queremos comprobar que nuestra clase UserManagementInternalService funciona como esperamos:

RSpec.describe UserManagementInternalService do
  subject { described_class.new(user: user) }
  
  let(:new_details) { name: 'Jane' }
  let(:user) { create(:user, name: 'Maria') }
  
  it 'returns updated details' do
    subject.update_user(new_details: new_details)

    expect(result[:updated_details][:name]).to eq(new_details[:name])
  end
end

Pero ojo, si hacemos esto tal cual, estaremos ejecutando el código del método update_user tal cual, realizando la llamada a la gema externa.

Necesitamos simular dicha llamada. Para ello vamos a utilizar el método instance_double, que nos permite simular la llamada a un método de instancia para un doble.

En primer lugar, definimos el doble de instancia de la clase para el servicio externo:

RSpec.describe UserManagementInternalService do
  subject { described_class.new(user: user) }
  
  let(:new_details) { name: 'Jane' }
  let(:user) { create(:user, name: 'Maria') }
  let(:user_management_external_service) { instance_double(UserManagementExternalService::Client, update_user: updater_response) }
  
  it 'returns updated details' do
    subject.update_user(new_details: new_details)

    expect(result[:updated_details][:name]).to eq(new_details[:name])
  end
end

Con esto estamos creando un doble que, al ser llamado con el método update_user, devolverá lo que hayamos definido en la variable updater_response.

Definimos pues la variable updater_response:

RSpec.describe UserManagementInternalService do
  subject { described_class.new(user: user) }
  
  let(:new_details) { name: 'Jane' }
  let(:user) { create(:user, name: 'Maria') }
  let(:user_management_external_service) { instance_double(UserManagementExternalService::Client, update_user: updater_response) }
  let(:updater_response) do
    {
      user: {
        name: new_details[:name]
      }
    }
  end
  
  it 'returns updated details' do
    subject.update_user(new_details: new_details)

    expect(result[:updated_details][:name]).to eq(new_details[:name])
  end
end

Lo que estamos diciendo es que cuando se llame al método update_user nos devuelva el contenido de updater_response, que como puedes ver es un hash con la información sobre la usuaria actualizada que normalmente nos devolvería el servicio externo.

TIP: Esta variable puede estar “hardcodeada” aquí o también podríamos guardarla en forma de fixtura.

Lo siguiente que debemos hacer es reemplazar la llamada al servicio externo en el flujo de trabajo. Para ello utilizamos el método allow:

RSpec.describe UserManagementInternalService do
  subject { described_class.new(user: user) }
  
  let(:new_details) { name: 'Jane' }
  let(:user) { create(:user, name: 'Maria') }
  let(:user_management_external_service) { instance_double(UserManagementExternalService::Client, update_user: updater_response) }
  let(:updater_response) do
    {
      user: {
        name: new_details[:name]
      }
    }
  end
  
  before do
    allow(UserManagement::Client).to receive(:new).and_return(user_management_external_service)
  end

  it 'returns updated details' do
    subject.update_user(new_details: new_details)

    expect(result[:updated_details][:name]).to eq(new_details[:name])
  end
end

Aquí le estamos diciendo que cuando alguien instancie el servicio externo en la aplicación, devuelva nuestro doble en vez de crear una instancia real.

En este caso he definido el allow dentro del bloque before, que se ejecuta por defecto antes de cada test, así que no tengo que volver a escribir el allow dentro de cada test.

¡Listo! Ahora ya podemos lanzar nuestras pruebas sin miedo a estar llamando de verdad al servicio externo de verdad, porque llamamos a su doble.

BONUS: Simular distintas respuestas

Puede que no siempre queramos la misma respuesta del servicio externo. Por ejemplo, es de esperar que cuando los parámetros sean inválidos, el servicio externo nos devuelva un error.

Para simular esto, podemos utilizar los contextos de RSpec para definir distintas respuestas con el mismo nombre de variable:

RSpec.describe UserManagementInternalService do
  subject { described_class.new(user: user) }
  
  let(:user) { create(:user, name: 'Maria') }
  let(:user_management_external_service) { instance_double(UserManagementExternalService::Client, update_user: updater_response) }
  
  before do
    allow(UserManagement::Client).to receive(:new).and_return(user_management_external_service)
  end

  context 'when details are valid' do
    let(:new_details) { name: 'Jane' }
    let(:updater_response) do
      {
        user: {
          name: new_details[:name]
        }
      }
    end

    it 'returns updated details' do
      result = subject.update_user(new_details: new_details)

      expect(result[:updated_details][:name]).to eq(new_details[:name])
    end
  end
  
  context 'when details are invalid' do
    let(:new_details) { invalid_detail: 'very bad' }
    let(:updater_response) do
      {
        user: nil,
        errors: ['User does not contain invalid_detail']
      }
    end

    it 'returns updated details' do
      subject.update_user(new_details: new_details)

      expect(result[:errors]).to eq(['User does not contain invalid_detail'])
    end
  end
end

¿Te ha gustado?

Si te ha parecido de utilidad, invítame a un Ko-Fi 😉

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: