Tests de integracion con Wallaby


Wallaby

Introducción

Para los desarrolladores que crean aplicaciones web con Elixir, contar con pruebas robustas es esencial para un proceso de desarrollo fluido y una buena experiencia de usuario. Wallaby es una poderosa biblioteca que eleva las capacidades de prueba en Elixir.


Si no puedes esperar a ver el código, este pull request ilustra todos los cambios necesarios para que funcione bien.


Entendiendo Wallaby

Wallaby opera automatizando interacciones dentro de un navegador web real. Esto te permite simular acciones de usuario como hacer clic en botones, rellenar formularios y verificar que los elementos correctos se muestran en la página. Este enfoque de pruebas end-to-end complementa las pruebas unitarias y de integración tradicionales.


Ventajas de Wallaby

Eficiencia: El soporte de Wallaby para la ejecución concurrente de pruebas acelera significativamente tu suite de pruebas, proporcionando retroalimentación más rápida.

Confianza: Al probar dentro de navegadores reales, puedes detectar problemas específicos de compatibilidad con los navegadores desde temprano, asegurando que tu aplicación funcione como se espera para tus usuarios.


Instalación

Agrega Wallaby como dependencia en tu archivo mix.exs:

{:wallaby, "~> 0.30", runtime: false, only: :test}

Luego instálalo con mix deps.get.


Configuración

Para usar Wallaby, ajusta los siguientes parámetros en tu archivo config/test.exs:

  • Endpoint: Establece server: true para habilitar tu endpoint Phoenix en pruebas.
  • SQL Sandbox: Establece sql_sandbox: true para pruebas aisladas de base de datos.
  • Configuración de Wallaby: Agrega un bloque de configuración de Wallaby, reemplazando tr con el nombre de tu aplicación: <br />
    config :tr, TrWeb.Endpoint,
    ...
    server: true
    
    config :tr, sql_sandbox: true
    
    config :wallaby,
    screenshot_on_failure: false,
    opt_app: :tr,
    driver: Wallaby.Chrome,
    chromedriver: [headless: true, binary: "/usr/bin/google-chrome-stable"]
    <br />

Instrumentando Tu Endpoint lib/tr_web/endpoint.ex

  • Agregar Configuración de Wallaby: Cerca de la parte superior de tu archivo lib/tr_web/endpoint.ex, inserta el siguiente bloque de configuración. Asegúrate de reemplazar tr con el nombre de tu aplicación real.
  • Modificar Configuración del Socket: Dentro de la función socket de tu endpoint, agrega un campo user_agent para establecer un vínculo entre la sesión del navegador y la base de datos.
 if Application.compile_env(:tr, :sql_sandbox) do
    plug Phoenix.Ecto.SQL.Sandbox
  end

 socket "/live", Phoenix.LiveView.Socket,
    websocket: [connect_info: [:user_agent, session: @session_options]]

La configuración de user_agent dentro del bloque socket asegura que Wallaby pueda asociar correctamente cada sesión de prueba en el navegador con las interacciones apropiadas en la base de datos.


Router

El siguiente paso es configurar el router. Básicamente, necesitamos establecer el hook on_mount en live_session. Hay dos formas de hacerlo, y veremos ambas porque esta aplicación utiliza la autenticación generada por Phoenix.

El primer bloque asegura que todas las sesiones predeterminadas de LiveView utilicen el hook.

  live_session :default, on_mount: TrWeb.Hooks.AllowEctoSandbox do
    scope "/blog", TrWeb do
      pipe_through :browser

      live "/", BlogLive, :index
      live "/:id", PostLive, :show
    end
  end

El segundo bloque añade el mismo hook para todas las demás LiveViews, especificándolo en el on_mount.

  ## Authentication routes
  scope "/", TrWeb do
    pipe_through [:browser, :redirect_if_user_is_authenticated]

    live_session :redirect_if_user_is_authenticated,
      on_mount: [
        {TrWeb.UserAuth, :redirect_if_user_is_authenticated},
        {TrWeb.Hooks.AllowEctoSandbox, :default}
      ] do
      live "/users/register", UserRegistrationLive, :new
      live "/users/log_in", UserLoginLive, :new
      live "/users/reset_password", UserForgotPasswordLive, :new
      live "/users/reset_password/:token", UserResetPasswordLive, :edit
    end

    post "/users/log_in", UserSessionController, :create
  end

  scope "/", TrWeb do
    pipe_through [:browser, :require_authenticated_user]

    live_session :require_authenticated_user,
      on_mount: [
        {TrWeb.UserAuth, :ensure_authenticated},
        {TrWeb.Hooks.AllowEctoSandbox, :default}
      ] do
      live "/users/settings", UserSettingsLive, :edit
      live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
    end
  end

  scope "/", TrWeb do
    pipe_through [:browser]

    delete "/users/log_out", UserSessionController, :delete

    live_session :current_user,
      on_mount: [{TrWeb.UserAuth, :mount_current_user}, {TrWeb.Hooks.AllowEctoSandbox, :default}] do
      live "/users/confirm/:token", UserConfirmationLive, :edit
      live "/users/confirm", UserConfirmationInstructionsLive, :new
    end
  end

Entendiendo el Propósito del Hook

Quizás te preguntes cuál es la función del “hook” que estamos a punto de agregar. Este fragmento de código juega un papel clave en la configuración del entorno de pruebas para Wallaby. Aunque hay varios lugares donde podrías colocarlo, lo haremos en lib/tr_web/sandbox.ex para simplificar.

¿Qué Hace el Hook?

Piensa en el hook como un ayudante que realiza las siguientes tareas importantes:

  • Prepara la Base de Datos: Asegura que cada prueba de Wallaby comience con una base de datos nueva e independiente, evitando conflictos entre pruebas.
  • Conecta Wallaby con Tu Aplicación: El hook crea un puente entre las sesiones de prueba del navegador de Wallaby y tu aplicación, lo que permite que Wallaby interactúe correctamente con tu código.

Este hook es esencial para asegurar que las pruebas con Wallaby funcionen sin problemas y que cada prueba se ejecute en un entorno controlado y aislado.


defmodule TrWeb.Hooks.AllowEctoSandbox do
  @moduledoc """
  Sandbox configuration for integration tests
  """
  import Phoenix.LiveView
  import Phoenix.Component

  def on_mount(:default, _params, _session, socket) do
    allow_ecto_sandbox(socket)
    {:cont, socket}
  end

  defp allow_ecto_sandbox(socket) do
    %{assigns: %{phoenix_ecto_sandbox: metadata}} =
      assign_new(socket, :phoenix_ecto_sandbox, fn ->
        if connected?(socket), do: get_connect_info(socket, :user_agent)
      end)

    Phoenix.Ecto.SQL.Sandbox.allow(metadata, Application.get_env(:your_app, :sandbox))
  end
end

El ultimo paso es tener un caso de uso para probar test/support/feature_case.ex:

defmodule TrWeb.FeatureCase do
  @moduledoc """
  Integration / Feature base configuration
  """
  use ExUnit.CaseTemplate

  using do
    quote do
      use Wallaby.DSL
    end
  end

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Tr.Repo)

    Ecto.Adapters.SQL.Sandbox.mode(Tr.Repo, {:shared, self()})

    metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(Tr.Repo, self())

    {:ok, session} = Wallaby.start_session(metadata: metadata)

    {:ok, session: session}
  end
end

Siguiente: test/test_helper.exs:

Definimos una funcion para iniciar sesion y de esa manera poder testear usuarios autenticados mas facilmente.

ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Tr.Repo, :manual)

{:ok, _} = Application.ensure_all_started(:wallaby)
Application.put_env(:wallaby, :base_url, TrWeb.Endpoint.url())

import Tr.AccountsFixtures
import TrWeb.FeatureCase
import Wallaby.Browser

defmodule TrWeb.TestHelpers do
  @moduledoc """
  Helper module for tests
  """
  def log_in(session) do
    user_remember_me = "_tr_web_user_remember_me"

    user = confirmed_user_fixture()
    user_token = Tr.Accounts.generate_user_session_token(user)

    endpoint_opts = Application.get_env(:tr, TrWeb.Endpoint)
    secret_key_base = Keyword.fetch!(endpoint_opts, :secret_key_base)

    conn =
      %Plug.Conn{secret_key_base: secret_key_base}
      |> Plug.Conn.put_resp_cookie(user_remember_me, user_token, sign: true)

    session
    |> visit("/")
    |> set_cookie(user_remember_me, conn.resp_cookies[user_remember_me][:value])

    {:ok, %{session: session, user: user}}
  end
end

Ajustando Pruebas de LiveView: La importancia de async: false

¿Te acordás de cómo configuramos el modo sandbox para que sea compartido en el archivo de pruebas de características? Esta configuración es una movida inteligente para mejorar la eficiencia en las pruebas, pero tiene un pequeño impacto en cómo escribimos las pruebas de LiveView.

¿Por qué el cambio?

Sandbox compartido: El modo compartido del sandbox significa que múltiples pruebas de Wallaby reutilizan el mismo entorno de base de datos. Esto es genial para la velocidad, pero requiere un poco de coordinación con LiveView.

Asegurando consistencia: Establecer async: false en las pruebas de LiveView ayuda a garantizar que cada prueba tenga una vista consistente de la base de datos. Esto previene resultados inesperados que podrían surgir con múltiples pruebas de LiveView corriendo al mismo tiempo.

  use TrWeb.ConnCase, async: false

Corriendo una prueba

En este ejemplo, ejecutamos 2 navegadores con usuarios logueados y verificamos que puedan enviar un mensaje y recibirlo en tiempo real. También verás ejemplos de cómo seleccionar e interactuar con diferentes elementos y correr algunas validaciones.

defmodule TrWeb.Integration.PostIntegrationTest do
  use TrWeb.FeatureCase, async: false
  use Wallaby.Feature
  use Phoenix.VerifiedRoutes, router: TrWeb.Router, endpoint: TrWeb.Endpoint

  import Wallaby.Query

  import TrWeb.TestHelpers

  @send_button button("Send")

  describe "Blog articles" do
    test "has some articles", %{session: session} do
      session
      |> visit("/blog")
      |> find(css("div #upgrading-k3s-with-system-upgrade-controller", count: 1))
      |> assert_has(css("h2", text: "Upgrading K3S with system-upgrade-controller"))
    end

    def message(msg), do: css("li", text: msg)

    @sessions 2
    feature "That users can send messages to each other", %{sessions: [user1, user2]} do
      page = ~p"/blog/upgrading-k3s-with-system-upgrade-controller"

      user1
      |> log_in()

      user1
      |> visit(page)

      user1
      |> fill_in(Query.text_field("Message"), with: "Hello user2!")

      user1
      |> click(@send_button)

      user2
      |> log_in()

      user2
      |> visit(page)

      user2
      |> click(Query.link("Reply"))

      user2
      |> fill_in(Query.text_field("Message"), with: "Hello user1!")

      user2
      |> click(@send_button)

      user1
      |> assert_has(message("Hello user1!"))
      |> assert_has(css("p", text: "Online: 2"))

      user2
      |> assert_has(message("Hello user2!"))
      |> assert_has(css("p", text: "Online: 2"))
    end
  end
end

Para ver los resultados dirigite a la página de comprobaciones aquí


Ejecutándolo en GitHub Actions

Si querés que las pruebas de Wallaby se ejecuten automáticamente como parte de tu sistema de integración continua (CI), hay un paso importante: instalar los drivers del navegador.

      - uses: nanasess/setup-chromedriver@v2
      - run: |
          export DISPLAY=:99
          chromedriver --url-base=/wd/hub &
          sudo Xvfb -ac :99 -screen 0 1920x1080x24 > /dev/null 2>&1 & # optional

Podés ver el archivo completo aquí


El Viaje Continúa

Esto es una introducción básica a Wallaby. En los próximos artículos, exploraremos características avanzadas, mejores prácticas y formas de optimizar tu proceso de pruebas. Si te resultó útil este tutorial, dejá un comentario.


Notas de cierre

Si hay algo que te gustaría ver implementado, probado, o explorado en este espacio, hacémelo saber…


Errata

Si encontrás algún error o tenés alguna sugerencia, por favor mandame un mensaje para que lo pueda corregir.



No tienes cuenta? Regístrate aqui

Ya registrado? Iniciar sesión a tu cuenta ahora.

Iniciar session con GitHub
Iniciar sesion con Google
  • Comentarios

    Online: 0

Por favor inicie sesión para poder escribir comentarios.

by Gabriel Garrido