Testing: Stoic API

En este tutorial se verá como realizar pruebas tanto para la API Rest, los esquemas y el formulario creado en los tutoriales anteriores.

Paso 1: Verificación del Ambiente Testing

En el directorio config/ se encuentra las distintas configuraciones para la conexión con la base de datos y otros elementos para distintos ambientes. dev, prod y test. Además de dos archivos especiales config.exs y runtime.exs.

  • dev: Configuraciones para el ambiente de desarrollo local.

  • prod: Configuraciones para el ambiente en producción.

  • test: Configuraciones para el ambiente de pruebas locales.

  • config.exs: Configuraciones en tiempo de compilación. No puede acceder a variables de entorno en tiempo de ejecución. Además llamará a dev, prod y test respectivamente según la variable de entorno MIX_ENV.

  • runtime.exs: Configuraciones que pueden acceder a variables de entorno en tiempo de ejecución (antes de iniciar la aplicación).

Los archivos importantes son config.exs y runtime.exs los otros archivos pueden ser estructurados según sea conveniente para la aplicación, y se pueden importar con import_config/1 dentro de config.exs aunque no dentro de runtime.exs (limitaciones técnicas).

Dentro del archivo config/test.exs podemos verificar que la conexión con la base de datos se esté realizando en una nueva base de datos de pruebas en modo Sandbox (que cualquier cambio sea efímero).

config/test.exs
config :stoic_quotes, StoicQuotes.Repo,
  database: Path.expand("../stoic_quotes_test.db", __DIR__),
  pool_size: 5,
  pool: Ecto.Adapters.SQL.Sandbox

También debemos borrar algunos archivos en la suit de pruebas debido a que solo son ejemplos predeterminados creados para probar la página de bienvenida.

Se deben borrar los siguientes archivos:

  • stoic_quotes_web/controllers/error_html_test.exs

  • stoic_quotes_web/controllers/error_json_test.exs

  • stoic_quotes_web/controllers/page_controller_test.exs

Podemos verificar que la suit de pruebas de ejecuta exitosamente con el comando

$ mix test

Y ver un resultado similar a lo siguiente:

Compiling 23 files (.ex)
Running ExUnit with seed: 93697, max_cases: 8

.........
Finished in 0.3 seconds (0.00s async, 0.3s sync)
9 tests, 0 failures

¿Dónde están esas 9 pruebas?. Están en el archivo test/stoic_quotes/quotes_test.exs que fue creado por el generador del contexto (mix phx.gen.context) usado en el tutorial de la api rest.

Paso 2: Pruebas de Ecto Schema

Se realizarán las pruebas de los esquemas creados. Se debe probar que los campos tengan su tipo de datos adecuado. Como recordatorio se muestra el esquema a probar.

lib/stoic_quotes/quotes/quote.ex
defmodule StoicQuotes.Quotes.Quote do
  use Ecto.Schema
  import Ecto.Changeset

  @optional_fields [:id, :inserted_at, :updated_at]

  schema "quotes" do
    field(:quote, :string)
    field(:author, :string)
    field(:source, :string)

    timestamps(type: :utc_datetime)
  end

  def fields() do
    __MODULE__.__schema__(:fields)
  end

  def required_fields() do
    fields() -- @optional_fields
  end

  @doc false
  def changeset(quote, attrs) do
    quote
    |> cast(attrs, fields())
    |> validate_required(required_fields())
    |> unsafe_validate_unique(:quote, StoicQuotes.Repo)
    |> unique_constraint(:quote)
  end

  @doc false
  def new(attrs \\ %{"author" => "", "quote" => "", "source" => ""}) do
    case changeset(%__MODULE__{}, attrs) do
      {_, changeset} -> changeset
      changeset -> changeset
    end
  end
end

Para crear la prueba se crea un nuevo archivo en test/stoic_quotes/quotes/schema_test.exs

test/stoic_quotes/quotes/schema_test.exs
defmodule StoicQuotes.Tests.Schemas.Quotes.QuoteSchemaTest do
  use StoicQuotes.DataCase
  alias StoicQuotes.Quotes.Quote

  describe "quote schema field and types tests" do
    test "that schema has the correct fields and types" do
      expected_fields_and_types = [
        {:id, :id},
        {:quote, :string},
        {:author, :string},
        {:source, :string},
        {:inserted_at, :utc_datetime},
        {:updated_at, :utc_datetime}
      ]

      actual_fields_and_types =
        for field <- Quote.__schema__(:fields) do
          type = Quote.__schema__(:type, field)
          {field, type}
        end

      assert MapSet.new(expected_fields_and_types) == MapSet.new(actual_fields_and_types)
    end
  end
end
  • defmodule StoicQuotes.Tests.Schemas.Quotes.QuoteSchemaTest: Nombre del módulo siempre debe terminar en Test.

  • use StoicQuotes.DataCase: Usamos las herramientas para crear pruebas unitarias las cuales vienen incluidas en Phoenix. En este caso es una prueba unitaria que utilizará la base de datos. Este módulo esta definido en el archivo test/support/data_case.ex.

  • alias StoicQuotes.Quotes.Quote: Asignamos un alias al esquema para usarlo más fácilmente.

  • describe "quote schema field and types tests": Crea un nuevo grupo para que varias pruebas estén cohesionadas en un mismo lugar.

  • test "that schema has the correct fields and types": Se crea la prueba unitaria que deberá ser implementada.

  • assert MapSet.new(expected_fields_and_types) == MapSet.new(actual_fields_and_types): El uso de assert permite informar el éxito o fracaso de una prueba, en este caso validamos que dos conjuntos sean iguales.

  • Quote.schema(:type, field): Devuelve el tipo de campo dentro del esquema.

Para ejecutar la prueba solo debemos usar mix test, pero si se desea solamente probar un archivo se puede proporcionar en el comando.

$ mix test/stoic_quotes/quotes/schema_test.exs

O tambien puede ser utilizando la función describe.

$ mix test --only describe:"quote schema field and types tests" test/stoic_quotes/quotes/schema_test.exs

Paso 3: Pruebas de Ecto Changeset

Ahora se realizarán las pruebas de las validaciones, esto permitirá determinar si las validaciones están correctamente establecidas y detectar cualquier problema con ellas.

test/stoic_quotes/quotes/schema_test.exs
describe "changeset/2" do
  test "that changeset with valid params is valid" do
    params = %{
      "author" => "Marcus Aurelius",
      "source" => "Meditations",
      "quote" =>
        "You have power over your mind — not outside events. Realize this, and you will find strength."
    }

    changeset = Quote.changeset(%Quote{}, params)

    assert %Ecto.Changeset{valid?: true, changes: _} = changeset
  end

  test "that changeset with invalid params is invalid" do
    params = %{
      "author" => "",
      "source" => "Meditations",
      "quote" =>
        "You have power over your mind — not outside events. Realize this, and you will find strength."
    }

    changeset = Quote.changeset(%Quote{}, params)

    assert %Ecto.Changeset{
             valid?: false,
             errors: [{:author, {"can't be blank", [validation: :required]}}]
           } =
             changeset

    params = %{
      "author" => "Marcus Aurelius",
      "source" => "",
      "quote" =>
        "You have power over your mind — not outside events. Realize this, and you will find strength."
    }

    changeset = Quote.changeset(%Quote{}, params)

    assert %Ecto.Changeset{
             valid?: false,
             errors: [{:source, {"can't be blank", [validation: :required]}}]
           } =
             changeset

    params = %{
      "author" => "Marcus Aurelius",
      "source" => "Meditations",
      "quote" => ""
    }

    changeset = Quote.changeset(%Quote{}, params)

    assert %Ecto.Changeset{
             valid?: false,
             errors: [{:quote, {"can't be blank", [validation: :required]}}]
           } =
             changeset
  end
end

También añadimos una validación para la función new/1 donde comprobaremos que siempre devuelva un changeset.

test/stoic_quotes/quotes/schema_test.exs
describe "new/1" do
  test "that returns a changeset" do
    params = %{
      "author" => "Marcus Aurelius",
      "source" => "Meditations",
      "quote" =>
        "You have power over your mind — not outside events. Realize this, and you will find strength."
    }

    changeset = Quote.new(params)

    assert %Ecto.Changeset{valid?: true, changes: _} = changeset
  end
end

Paso 4: Pruebas de Contexto

Las pruebas de contexto fueron creadas automáticamente por el generador phx.gen.context. Se pueden ver dentro del archivo test/stoic_quotes/quotes_test.exs, pero se recomienda mover el archivo al directorio test/stoic_quotes/quotes/quotes_test.exs para que tenga cohesión con las pruebas del esquema test/stoic_quotes/quotes/schema_test.exs.

test/stoic_quotes/quotes_test.exs
defmodule StoicQuotes.Tests.Contexts.QuotesContextTest do
  use StoicQuotes.DataCase

  alias StoicQuotes.Quotes

  describe "quotes" do
    alias StoicQuotes.Quotes.Quote

    import StoicQuotes.QuotesFixtures

    @invalid_attrs %{author: nil, source: nil, quote: nil}

    test "list_quotes/0 returns all quotes" do
      quote = quote_fixture()
      assert Quotes.list_quotes() == [quote]
    end
# ...

Lo que se puede destacar es el uso de Fixtures (import StoicQuotes.QuotesFixtures). Esto es una herramienta de pruebas que permite tener un entorno predefinido por ejemplo archivos o valores de base de datos que facilitan la creación de pruebas.

Si vamos al archivo test/support/fixtures/quotes_fixtures.ex veremos que simplemente genera un nuevo registro en la base de datos y es utilizado en las pruebas de contexto como quote = quote_fixture().

test/support/fixtures/quotes_fixtures.ex
defmodule StoicQuotes.QuotesFixtures do
  @moduledoc """
  This module defines test helpers for creating
  entities via the `StoicQuotes.Quotes` context.
  """

  @doc """
  Generate a quote.
  """
  def quote_fixture(attrs \\ %{}) do
    {:ok, quote} =
      attrs
      |> Enum.into(%{
        author: "some author",
        quote: "some quote",
        source: "some source"
      })
      |> StoicQuotes.Quotes.create_quote()

    quote
  end
end

Paso 5: Pruebas de Endpoint Rest

Para probar los endpoints rest debemos crear un nuevo archivo en test/stoic_quotes_web/controllers/quotes_controller_test.exs.

Para esto probaremos los endpoints definidos en el router

router.ex
scope "/api", StoicQuotesWeb do
  pipe_through(:api)
  get("/quotes", QuotesController, :index)
  get("/quotes/random", QuotesController, :show)
end
test/stoic_quotes_web/controllers/quotes_controller_test.exs
defmodule StoicQuotesWeb.Tests.Controllers.QuotesControllerTest do
  use StoicQuotesWeb.ConnCase
  import StoicQuotes.QuotesFixtures

  describe "/api/quotes" do
    test "GET /api/quotes", %{conn: conn} do
      quote_fixture(%{quote: "1"})
      quote_fixture(%{quote: "2"})

      conn = get(conn, ~p"/api/quotes")

      assert [
               %{
                 "author" => "some author",
                 "quote" => "1",
                 "source" => "some source"
               },
               %{
                 "author" => "some author",
                 "quote" => "2",
                 "source" => "some source"
               }
             ] = json_response(conn, 200)["data"]
    end

    test "GET /api/quotes/random", %{conn: conn} do
      quote_fixture()
      conn = get(conn, ~p"/api/quotes/random")

      assert %{
               "author" => "some author",
               "quote" => "some quote",
               "source" => "some source"
             } = json_response(conn, 200)["data"]
    end
  end
end
  • use StoicQuotesWeb.ConnCase: Debemos usar el tipo de prueba ConnCase para poder acceder a levantar el servidor y realizar pruebas. Este módulo esta definido en el archivo test/support/conn_case.ex.

  • conn = get(conn, ~p"/api/quotes"): Obtenemos el resultado de llamar al endpoint /api/quotes.

  • json_response(conn, 200)["data"]: Obtenemos la respuesta en formato json y el contenido de la propiedad "data" para realizar la comparación.

  • ~p"/api/quotes": ~p es un sigilo (macro) de Phoenix que permite verificar que la ruta ingresada existe en el router, lo cual es muy recomendable. Es parte de lo que se conoce como verified routes.

Paso 6: Pruebas de LiveView

Ahora se realizarán las pruebas del formulario hecho con LiveView. El cual se muestra en la siguiente ruta.

router.ex
scope "/", StoicQuotesWeb do
  pipe_through(:browser)

  live("/", Live.QuotesForm, :live)
end
test/stoic_quotes_web/live/quotes_form_test.exs
defmodule StoicQuotesWeb.Tests.Live.QuotesFormTest do
  use StoicQuotesWeb.ConnCase
  import Phoenix.LiveViewTest

  describe "LiveView quotes form page tests" do
    test "that valid form saving is done", %{conn: conn} do
      {:ok, lv, _html} =
        live(
          conn,
          ~p"/"
        )

      lv
      |> form("form", %{
        "author" => "some author",
        "source" => "some source",
        "quote" => "some quote"
      })
      |> render_submit()

      conn = get(conn, ~p"/api/quotes")

      assert [
               %{
                 "author" => "some author",
                 "quote" => "some quote",
                 "source" => "some source"
               }
             ] = json_response(conn, 200)["data"]
    end

    test "that invalid form shows errors", %{conn: conn} do
      {:ok, lv, _html} =
        live(
          conn,
          ~p"/"
        )

      result =
        lv
        |> form("form", %{
          "author" => "",
          "source" => "",
          "quote" => ""
        })
        |> render_submit()

      assert result =~ "can&#39;t be blank"
    end

    test "that valid form cannot save duplicates", %{conn: conn} do
      {:ok, lv, _html} =
        live(
          conn,
          ~p"/"
        )

      lv
      |> form("form", %{
        "author" => "some author",
        "source" => "some source",
        "quote" => "some quote"
      })
      |> render_submit()

      result =
        lv
        |> form("form", %{
          "author" => "some author",
          "source" => "some source",
          "quote" => "some quote"
        })
        |> render_submit()

      assert result =~ "There was an error saving the Quote"
    end
  end
end

Podemos ver que realizar pruebas con LiveView es muy similar a realizar pruebas con endpoints json. Sin embargo hay algunos códigos que se deben explicar como los siguientes:

El siguiente código inicializa la estructura de lv que puede ser usada por otras funciones para renderizar la página.

{:ok, lv, _html} =
  live(
    conn,
    ~p"/"
  )

En el siguiente código se llama a funciones especiales de LiveView como form("elemento html", parametros) y render_submit() que permiten realizar el envío de un formulario.

lv
|> form("form", %{
  "author" => "",
  "source" => "",
  "quote" => ""
})
|> render_submit()

Pasos Finales

Se ha realizado una aplicación completa con Phoenix y LiveView desde una simple API Rest a una suite de pruebas unitarias.

Se pueden ver los siguientes conceptos para profundizar: