Inicio / Elixir / Elixir: Programación Funcional y Concurrente / Testing con ExUnit

Testing con ExUnit

ExUnit, assert, setup, describe, mocking y doctests.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Testing con ExUnit en Elixir

ExUnit es el framework de testing integrado en Elixir. Viene incluido sin necesidad de dependencias adicionales y proporciona herramientas para escribir tests unitarios, de integración y basados en propiedades. La cultura de testing en Elixir es fuerte, y herramientas como doctests hacen que documentar y testear sean la misma actividad.

Configuración Básica

Todo proyecto Elixir viene con ExUnit configurado:

# test/test_helper.exs
ExUnit.start()

# test/mi_modulo_test.exs
defmodule MiModuloTest do
  use ExUnit.Case

  test "suma dos números" do
    assert 2 + 3 == 5
  end
end

Ejecutar tests:

# mix test                    — Todos los tests
# mix test test/mi_test.exs   — Un archivo específico
# mix test test/mi_test.exs:5 — Una línea específica
# mix test --trace            — Output detallado
# mix test --stale            — Solo tests afectados por cambios

Assertions

ExUnit proporciona macros de aserción expresivas:

defmodule AssercionesTest do
  use ExUnit.Case

  test "assertions básicas" do
    assert 1 + 1 == 2
    refute 1 + 1 == 3

    assert "hola" =~ "ho"       # Regex o substring
    assert_in_delta 3.14, 3.1, 0.1  # Comparación con tolerancia
  end

  test "assertions con pattern matching" do
    resultado = {:ok, %{nombre: "Ana"}}
    assert {:ok, %{nombre: nombre}} = resultado
    assert nombre == "Ana"
  end

  test "assertions de excepciones" do
    assert_raise ArithmeticError, fn ->
      1 / 0
    end

    assert_raise ArgumentError, "argumento inválido", fn ->
      raise ArgumentError, "argumento inválido"
    end
  end

  test "assertions de mensajes" do
    send(self(), {:evento, :usuario_creado})

    assert_received {:evento, :usuario_creado}
    refute_received {:evento, :usuario_eliminado}
  end
end

Setup y Describe

setup ejecuta código antes de cada test, y describe agrupa tests relacionados:

defmodule CalculadoraTest do
  use ExUnit.Case

  # Setup global para todos los tests del módulo
  setup do
    calculadora = %{precision: 2}
    {:ok, calc: calculadora}
  end

  describe "operaciones básicas" do
    test "suma correctamente", %{calc: calc} do
      resultado = Float.round(2.1 + 3.2, calc.precision)
      assert resultado == 5.3
    end

    test "resta correctamente", %{calc: _calc} do
      assert 10 - 3 == 7
    end
  end

  describe "validaciones" do
    setup do
      # Setup adicional solo para este grupo
      {:ok, limites: %{min: 0, max: 1000}}
    end

    test "rechaza valores negativos", %{limites: limites} do
      assert -5 < limites.min
    end

    test "acepta valores en rango", %{limites: limites} do
      assert 500 >= limites.min and 500 <= limites.max
    end
  end
end

Setup con Contexto Compartido

setup_all se ejecuta una sola vez antes de todos los tests del módulo:

defmodule IntegracionTest do
  use ExUnit.Case

  setup_all do
    # Se ejecuta una vez antes de todos los tests
    {:ok, pid} = MiApp.Cache.start_link([])
    %{cache_pid: pid}
  end

  setup %{cache_pid: pid} do
    # Se ejecuta antes de cada test
    MiApp.Cache.limpiar(pid)
    :ok
  end

  test "almacena y recupera valores", %{cache_pid: pid} do
    MiApp.Cache.put(pid, :clave, "valor")
    assert MiApp.Cache.get(pid, :clave) == "valor"
  end
end

Doctests

Los doctests permiten escribir tests directamente en la documentación:

defmodule Matematica do
  @doc """
  Calcula el factorial de un número.

  ## Ejemplos

      iex> Matematica.factorial(0)
      1

      iex> Matematica.factorial(5)
      120

      iex> Matematica.factorial(10)
      3628800
  """
  def factorial(0), do: 1
  def factorial(n) when n > 0, do: n * factorial(n - 1)

  @doc """
  Verifica si un número es primo.

  ## Ejemplos

      iex> Matematica.primo?(2)
      true

      iex> Matematica.primo?(4)
      false

      iex> Matematica.primo?(17)
      true
  """
  def primo?(n) when n < 2, do: false
  def primo?(2), do: true
  def primo?(n) do
    not Enum.any?(2..trunc(:math.sqrt(n)), &(rem(n, &1) == 0))
  end
end

# En el test:
defmodule MatematicaTest do
  use ExUnit.Case
  doctest Matematica
end

Mocking con Mox

La biblioteca Mox permite crear mocks basados en behaviours:

# Definir un behaviour
defmodule MiApp.ClienteHTTP do
  @callback get(String.t()) :: {:ok, map()} | {:error, term()}
end

# En test/test_helper.exs
Mox.defmock(MiApp.MockHTTP, for: MiApp.ClienteHTTP)

# En el test
defmodule MiApp.ServicioTest do
  use ExUnit.Case
  import Mox

  setup :verify_on_exit!

  test "procesa respuesta exitosa" do
    expect(MiApp.MockHTTP, :get, fn url ->
      assert url == "https://api.example.com/datos"
      {:ok, %{status: 200, body: ~s({"nombre": "test"})}}
    end)

    assert {:ok, _} = MiApp.Servicio.obtener_datos(MiApp.MockHTTP)
  end

  test "maneja error de red" do
    expect(MiApp.MockHTTP, :get, fn _url ->
      {:error, :timeout}
    end)

    assert {:error, :timeout} = MiApp.Servicio.obtener_datos(MiApp.MockHTTP)
  end
end

Tags y Filtros

Los tags permiten categorizar y filtrar tests:

defmodule LentoTest do
  use ExUnit.Case

  @tag :integracion
  test "test de integración con base de datos" do
    # ...
  end

  @tag :slow
  @tag timeout: 120_000
  test "operación lenta" do
    # ...
  end

  @tag :skip
  test "test deshabilitado" do
    # No se ejecutará
  end
end

# Ejecutar solo tests con un tag:
# mix test --only integracion
# mix test --exclude slow

Resumen

ExUnit proporciona un framework completo para testing en Elixir: assertions expresivas que aprovechan el pattern matching, describe y setup para organizar tests, doctests para unificar documentación y verificación, mocking con Mox para aislar dependencias, y tags para categorizar y filtrar la ejecución. La integración profunda de ExUnit con el lenguaje y Mix hace que testear sea una actividad natural y productiva en el desarrollo con Elixir.

🔒

Ejercicio práctico disponible

Testing con ExUnit

Desbloquear ejercicios
// Testing con ExUnit
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes