Mock en elixir, pero fácil
Una manera sencilla de probar dependencias externas sin tener que depender de otra dep. Fácil, directo, sencillo.
Una de las cosas que no disfruto del todo pero que considero es sumamente importtante a la hora de escribir código son las PRUEBAS. Ya sea que a ti si te guste escribir pruebas o escribas pruebas porque eres responsable y sabes que se tienen que implementar para un mejor código (como es mi caso) hay un problema que suele darse con frecuencia y este es:
¿Cómo mockear dependencias externas?
Existen muchas bibliotecas de mocking, como mock
, mox
o mimic
, y todas funcionan bastante bien. AppSignal incluso creó una guía útil sobre ellas, que puedes consultar aquí.
Sin embargo, me gustaría proponer una alternativa mucho más sencilla y sin dependencias para simular tus dependencias externas. De hecho, ¡el mismo José Valim ya escribió sobre este enfoque en 2015! La idea es simple: simplemente reemplaza tu módulo de dependencia con un mock durante las pruebas.
Esta técnica es ideal si buscas mantener tu código limpio y enfocado, evitando agregar dependencias innecesarias.
Un ejemplo claro, digamos que estas implementando Stripe:
defmodule FoxApp.Invoices do
def get_invoice_url(invoice_id) do
case Stripe.Invoice.retrieve(invoice_id) do
{:ok, %Stripe.Invoice{hosted_invoice_url: url}} -> {:ok, url}
{:error, %Stripe.ApiErrors{message: message}} -> {:error, message}
end
end
end
Este código utiliza la fantástica biblioteca StripityStripe para interactuar con la API de Stripe. Pero, ¿cómo podemos simular esta dependencia en nuestras pruebas? ¡Simple! Adaptamos el módulo de Stripe que usa nuestro código según el entorno en el que estemos trabajando.
# config/config.exs
config :my_app, stripe: Stripe
# config/test.exs
config :my_app, stripe: FoxApp.Support.StripeMock
Y en nuestro código, hacemos fetch de la dependencia en tiempo de compilación
# lib/fox_app/invoices.ex
defmodule FoxApp.Invoices do
@stripe Application.compile_env(:fox_app, :stripe)
def get_invoice_url(invoice_id) do
case @stripe.Invoice.retrieve(invoice_id) do
{:ok, %Stripe.Invoice{hosted_invoice_url: url}} -> {:ok, url}
{:error, %Stripe.ApiErrors{message: message}} -> {:error, message}
end
end
end
Listo, ahora que ya tenemos nuestra configuración lista veamos como podemos usarla escribiendo unas pruebas:
# test/my_app/invoices_test.exs
defmodule FoxApp.InvoicesTest do
use FoxApp.DataCase, async: true
alias FoxApp.Invoices
describe "get_invoice_url/1" do
test "returns an invoice url" do
assert {:ok, url} = Invoices.get_invoice_url("in_12345")
assert url == "https://stripe.com/invoice/in_12345"
assert_receive {:retrieve, "in_12345"}
end
end
end
La prueba que acabamos de escribir espera recibir el invoice url donde espera una respuesta por parte de nuestro mock
# test/support/stripe_mock.ex
defmodule FoxApp.Support.StripeMock do
defmodule Invoice do
def retrieve(invoice_id) do
send(self(), {:retrieve, invoice_id})
{:ok, %Stripe.Invoice{hosted_invoice_url: "https://stripe.com/invoices/#{invoice_id}"}}
end
end
end
El mock por otra parte manda un mensaje a el test case asegurandose que nuestra lógica se cumpla. De esta manera hacemos un assert no solo para que el mock sea llamado, sino que también sea llamado con los argumentos correctos.
Y eso es todo. Uso este patrón en todos mis proyectos y tiendo a reemplazar los mocks existentes con él cuando es posible. Es ligero, pero lo suficientemente expresivo como para soportar incluso grandes conjuntos de pruebas. Me gusta que no tengo que repetir la misma configuración del mock una y otra vez para cada caso de prueba y archivo, como sucede con algunas bibliotecas de mocking. Defino un mock una vez y puedo usarlo en cualquier parte. También me agrada que la configuración en el código de mi aplicación se reduce a una sola línea al inicio del archivo.