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á adev
,prod
ytest
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 :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.
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
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 enTest
. -
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 archivotest/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 deassert
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.
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.
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
.
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()
.
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
scope "/api", StoicQuotesWeb do
pipe_through(:api)
get("/quotes", QuotesController, :index)
get("/quotes/random", QuotesController, :show)
end
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 archivotest/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.
scope "/", StoicQuotesWeb do
pipe_through(:browser)
live("/", Live.QuotesForm, :live)
end
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'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()