Inicio / Elixir / Phoenix Framework: Web en Tiempo Real / Testing en Phoenix

Testing en Phoenix

ConnTest, DataCase, feature tests, mocks y factories.

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 en Phoenix

Introducción

Phoenix incluye un ecosistema de testing completo basado en ExUnit. Desde tests de controladores con ConnTest hasta tests de LiveView y channels, cada capa tiene herramientas especializadas.

ConnTest: Testing de Controladores

ConnTest provee helpers para simular peticiones HTTP:

defmodule MyAppWeb.ProductoControllerTest do
  use MyAppWeb.ConnCase

  alias MyApp.Catalogo

  @valid_attrs %{nombre: "Laptop", precio: 999.99}
  @invalid_attrs %{nombre: nil, precio: nil}

  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "GET /api/productos" do
    test "lista todos los productos", %{conn: conn} do
      {:ok, _} = Catalogo.create_producto(@valid_attrs)

      conn = get(conn, ~p"/api/productos")
      assert [%{"nombre" => "Laptop"}] = json_response(conn, 200)["data"]
    end
  end

  describe "POST /api/productos" do
    test "crea producto con datos válidos", %{conn: conn} do
      conn = post(conn, ~p"/api/productos", producto: @valid_attrs)
      assert %{"id" => id} = json_response(conn, 201)["data"]

      conn = get(conn, ~p"/api/productos/#{id}")
      assert %{"nombre" => "Laptop"} = json_response(conn, 200)["data"]
    end

    test "retorna errores con datos inválidos", %{conn: conn} do
      conn = post(conn, ~p"/api/productos", producto: @invalid_attrs)
      assert json_response(conn, 422)["errors"] != %{}
    end
  end
end

Helpers get/post/put/delete

Los helpers simulan cada método HTTP:

test "operaciones CRUD completas", %{conn: conn} do
  # Crear
  conn = post(conn, ~p"/api/items", item: %{titulo: "Test"})
  assert %{"id" => id} = json_response(conn, 201)["data"]

  # Leer
  conn = get(conn, ~p"/api/items/#{id}")
  assert json_response(conn, 200)["data"]["titulo"] == "Test"

  # Actualizar
  conn = put(conn, ~p"/api/items/#{id}", item: %{titulo: "Actualizado"})
  assert json_response(conn, 200)["data"]["titulo"] == "Actualizado"

  # Eliminar
  conn = delete(conn, ~p"/api/items/#{id}")
  assert response(conn, 204)
end

LiveViewTest

Testing de LiveView con live/2 y helpers de interacción:

defmodule MyAppWeb.ContadorLiveTest do
  use MyAppWeb.ConnCase
  import Phoenix.LiveViewTest

  test "incrementa el contador", %{conn: conn} do
    {:ok, view, html} = live(conn, ~p"/contador")
    assert html =~ "Valor: 0"

    assert view
           |> element("button", "Incrementar")
           |> render_click() =~ "Valor: 1"
  end

  test "actualiza con formulario", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/buscar")

    resultado =
      view
      |> form("#buscar-form", %{q: "elixir"})
      |> render_submit()

    assert resultado =~ "Resultados para: elixir"
  end

  test "navegación con live_patch", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/productos")

    assert view
           |> element("a", "Siguiente")
           |> render_click()

    assert_patch(view, ~p"/productos?page=2")
  end
end

Channel Testing

Tests para canales WebSocket:

defmodule MyAppWeb.SalaChannelTest do
  use MyAppWeb.ChannelCase

  setup do
    {:ok, _, socket} =
      MyAppWeb.UserSocket
      |> socket("user_id", %{user_id: 1})
      |> subscribe_and_join(MyAppWeb.SalaChannel, "sala:lobby")

    %{socket: socket}
  end

  test "enviar mensaje broadcast a todos", %{socket: socket} do
    push(socket, "nuevo_mensaje", %{"body" => "hola"})
    assert_broadcast "nuevo_mensaje", %{"body" => "hola"}
  end

  test "responde con mensajes recientes al unirse", %{socket: _socket} do
    assert_push "mensajes_recientes", %{mensajes: _}
  end
end

DataCase para Contextos

DataCase configura el sandbox de base de datos para tests de lógica:

defmodule MyApp.CatalogoTest do
  use MyApp.DataCase

  alias MyApp.Catalogo

  describe "productos" do
    test "list_productos/0 retorna todos los productos" do
      producto = producto_fixture()
      assert Catalogo.list_productos() == [producto]
    end

    test "create_producto/1 con datos válidos" do
      attrs = %{nombre: "Monitor", precio: 299.99}
      assert {:ok, producto} = Catalogo.create_producto(attrs)
      assert producto.nombre == "Monitor"
    end

    test "create_producto/1 con datos inválidos" do
      assert {:error, %Ecto.Changeset{}} = Catalogo.create_producto(%{nombre: nil})
    end
  end
end

Factories con ExMachina

ExMachina simplifica la creación de datos de test:

defmodule MyApp.Factory do
  use ExMachina.Ecto, repo: MyApp.Repo

  def usuario_factory do
    %MyApp.Cuentas.Usuario{
      nombre: sequence(:nombre, &"Usuario #{&1}"),
      email: sequence(:email, &"user#{&1}@test.com"),
      password_hash: Bcrypt.hash_pwd_salt("password123")
    }
  end

  def producto_factory do
    %MyApp.Catalogo.Producto{
      nombre: sequence(:nombre, &"Producto #{&1}"),
      precio: Decimal.new("29.99"),
      stock: 10
    }
  end
end

# Uso en tests
test "usuario puede comprar producto" do
  usuario = insert(:usuario)
  producto = insert(:producto, precio: Decimal.new("50.00"))
  assert {:ok, _pedido} = Pedidos.crear(usuario, producto)
end

Mocking con Mox

Mox permite definir mocks basados en behaviours:

# Definir behaviour
defmodule MyApp.PaymentGateway do
  @callback procesar_pago(map()) :: {:ok, map()} | {:error, String.t()}
end

# En test_helper.exs
Mox.defmock(MyApp.MockPayment, for: MyApp.PaymentGateway)

# En el test
test "procesa pago exitosamente" do
  expect(MyApp.MockPayment, :procesar_pago, fn params ->
    assert params.monto == 100
    {:ok, %{transaccion_id: "tx_123"}}
  end)

  assert {:ok, resultado} = Pedidos.pagar(pedido, MyApp.MockPayment)
  assert resultado.transaccion_id == "tx_123"
end

Resumen

Phoenix ofrece herramientas de testing para cada capa: ConnTest para controladores HTTP, LiveViewTest para interfaces en tiempo real, ChannelCase para WebSockets, DataCase para lógica de negocio. ExMachina simplifica factories y Mox provee mocking seguro basado en behaviours.

🔒

Ejercicio práctico disponible

Testing helpers y assertions

Desbloquear ejercicios
// Testing helpers y assertions
// 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