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 😉
Leave a Reply