Testing in Elixir
Elixir Testing Vinicius NegrisoloMy goal here:
Testing help developers to speed up, so let’s talk testing in Elixir.
Test to be Fast ⚡️
How to Get Productive in Elixir
The Dream
- Functional
- Immutability
- Compiled
- Performance
- Pattern Matching
- Fault Tolerance
- Concurrent
- Simpler than OO
- …
The Saga begins
Documentation / Tutorials
Blog posts (Elixir Radar / ElixirWeekly)
Meetups
Conferences
Small apps and libraries
Test Feedback Cycle
- defines development speed
- ⚡️ fast
- ✅ reliable
Test Situation
- test suite
- brittle / intermittent / gaps
- 11 min to run / all sequential
- manual tests on staging
- Continuous Integration
- rerun if test fails? really?
How to revert this scenario?
- test
- remove brittleness
- fill the gaps
- speed and paralelism
Test data
A Big Win!
Test data - Situation
- module attrs with fixed values
- setup to build data
- attrs, setup and assertions were far apart
Test data - Code Situation
@attrs %{
email: "user@mail.com",
full_name: "Billy Bob",
age: 30,
gender: :male
}
setup do
user = @attrs |> User.new() |> Repo.insert!()
[user: user]
end
test "do something", %{user: user} do
...
assert page.user_full_name == user.full_name
end
Test data - Factory
- Keep it simple
- A single factory definition per schema
- Random data all the time
- Build all regular attributes
- Build all belongs_to relation
- Everything else defined on tests
Test data - Factory Sample
defmodule Factory do
use MapBot
def new(User) do
%User{
email: &"#{&1}_#{Faker.Internet.email()}",
full_name: Faker.Name.name(),
age: 15 + :rand.uniform(100),
gender: Enum.random(~w(male female)a)
}
end
end
user = Factory.build(User, age: 17)
%User{} = Factory.insert!(User, age: 21)
Test data - Factory Benefits
- no database cycles
- no need to open factory definitions
- no assertions on hardcoded factory values
- schema changes <=> factory changes
- new test scenarios <=> no factory changes
Unit Testing
- single level of describe
- describe/test/assert/refute
- async false by default
- async by test file
Unit Testing
defmodule MyApp.Ecto.NormalizerTest do
use ExUnit.Case, async: true
alias MyApp.Ecto.Normalizer
describe "trim/2" do
test "trims a valid changeset for single atom" do
attrs = %{name: " Bob ", email: "bob@mail.com"}
changeset = attrs |> changeset() |> Normalizer.trim(:name)
assert changeset.changes.name == "Bob"
end
end
defp changeset(attrs) do
...
end
end
Unit Testing - Pattern Matching
- flip the assertion
- add/remove the pin
Unit Testing - Pattern Matching
assert changeset.changes.name == "Bob"
assert changeset.changes.name == full_name
assert changeset.changes.name == user.name
assert %{name: "Bob"} = changeset.changes
assert %{name: ^full_name} = changeset.changes
name = user.name
assert %{name: ^name} = changeset.changes
Unit Testing - Data Table
- controlled input/output
- scale up tests
Unit Testing - Data Table
defmodule MyApp.Ecto.Type.MoneyTest do
use ExUnit.Case, async: true
alias MyApp.Ecto.Type.Money
@cast_data [
{nil, {:ok, nil}},
{"314", {:ok, 314.00}},
{"314.59", {:ok, 314.59}},
{314, {:ok, 314.00}},
{314.59, {:ok, 314.59}}
]
describe "cast/1" do
for {value, expected} <- @cast_data do
@value value
@expected expected
test "casts '#{inspect(@value)}'" do
assert Money.cast(@value) == @expected
end
end
end
end
Unit Testing - Properties
- inputs are random and abundant
- catches some unthinkable edge cases
Unit Testing - Properties
defmodule MyApp.Ecto.Type.MoneyTest do
use ExUnit.Case, async: true
use ExUnitProperties
alias MyApp.Ecto.Type.Money
describe "cast/1, dump/1 and load/1" do
property "cast/1, dump/1, load/1 binary dollars+cents" do
check all dollars <- integer(),
cents <- 0..99 |> integer() do
value = "#{dollars}.#{cents}"
{expected, ""} = Float.parse(value)
assert {:ok, value} = Money.cast(value)
assert {:ok, value} = Money.dump(value)
assert Money.load(value) == {:ok, expected}
end
end
end
end
Special Test Cases
Controller / WebSocket / Feature
Controller Testing
use MyAppWeb.ConnCase
- exposes
Plug.Conn
- http request/response
Controller Testing
defmodule MyAppWeb.PostControllerTest do
use MyAppWeb.ConnCase, async: true
describe "show/2 when the user is authenticated" do
setup [:authenticated_conn_setup]
test "renders show page", %{conn: conn, current_user: user} do
post = insert!(Post, user: user)
conn = get(conn, Routes.post_path(conn, :show, post))
assert response = html_response(conn, 200)
assert response =~ "Post"
end
end
end
WebSocket Testing
use MyAppWeb.ChannelCase
- exposes
Phoenix.Socket
- channel join/push/reply/broadcast
- joined channels runs on new pids
- ecto sandbox allow for async
WebSocket Testing
defmodule MyAppWeb.UserChannelTest do
use MyAppWeb.ChannelCase, async: true
describe "handle_in/3 for list-posts when joined on user:lobby" do
setup [:authenticated_user_socket_setup, :join_user_lobby_setup]
test "returns all user posts", %{socket: socket, current_user: user} do
post = insert!(Post, user: user)
ref = push(socket, "list-posts")
assert_reply(ref, :ok, response, 500)
assert response == %{posts: [%{name: post.name}]}
end
end
def join_user_lobby_setup(%{socket: socket}) do
{:ok, _reply, socket} = subscribe_and_join(socket, UserChannel, "user:lobby")
Ecto.Adapters.SQL.Sandbox.allow(MyApp.Repo, self(), socket.channel_pid)
[socket: socket]
end
end
Feature Testing
- covers all the layers
- slower, but not slow
- can be parallel
wallaby
/hound
Feature Testing
defmodule MyAppWeb.Features.PostTest do
use MyAppWeb.FeatureCase, async: true
import Wallaby.Query, only: [css: 1, link: 1]
describe "when the user is authenticated" do
setup [:authenticated_session_setup]
test "posts page", %{session: session, current_user: user} do
post = insert!(Post, user: user)
session = visit(session, "/posts")
assert "Posts" in texts_by(session, css("h1"))
assert "Name" in texts_by(session, css("table th"))
assert post.name in texts_by(session, css("table td"))
session = click(session, link(post.name))
assert current_path(session) == "/posts/#{post.id}"
end
end
end
Solid and Fast Test suite
- Developers run locally
- faster feedback cycle
- oftener and smaller deploys
- easier to manage deploys
- less bugs
- builds your confidence