Getting started with Wallaby integration tests
Introduction
For developers building web applications with Elixir, robust testing is essential for a smooth development process and user experience. Wallaby is a powerful library that elevates your Elixir testing capabilities.
If you cannot wait to read the code, this pull requests illustrates all the changes needed for it to work well.
Understanding Wallaby
Wallaby operates by automating interactions within a real web browser. This allows you to simulate user actions like clicking buttons, filling out forms, and verifying that the correct elements are displayed on the page. This end-to-end testing approach complements traditional unit and integration tests.
Advantages of Wallaby
Efficiency: Wallaby’s support for concurrent test execution significantly speeds up your test suite, providing faster feedback.
Confidence: By testing within real browsers, you can catch browser-specific compatibility issues early on, ensuring your application functions as expected for your users.
Installation
Add Wallaby as a dependency in your mix.exs
file:
{:wallaby, "~> 0.30", runtime: false, only: :test}
Then install it with mix deps.get
Configuration
To use Wallaby, adjust the following settings in your config/test.exs
file:
-
Endpoint: Set
server: true
to enable your Phoenix endpoint in testing. -
SQL Sandbox: Set
sql_sandbox: true
for isolated database testing. -
Wallaby Configuration: Add a Wallaby configuration block, replacing
tr
with your application’s name: <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"]
Instrumenting Your Endpoint lib/tr_web/endpoint.ex
-
Add Wallaby Configuration: Near the top of your
lib/tr_web/endpoint.ex
file, insert the following configuration block. Be sure to replacetr
with your actual application name. -
Modify Socket Configuration: Within the socket function of your endpoint, add a
user_agent
field to establish a link between the browser session and the database.
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]]
The user_agent
setting within the socket configuration ensures Wallaby can correctly associate each test browser session with the appropriate database interactions.
Router
The next step is to setup the router, basically we need to setup the on_mount
hook in live_session, there are two ways
to do it, and we will see both because this application uses the phoenix generated authentication.
The first block makes sure that all the default LiveView sessions uses the 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
The second block adds the same hook for all the remaining LiveViews, specifying it in the 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
Understanding the Hook’s Purpose
You might be curious about the “hook” we’re about to add. This piece of code plays a key role in setting up the testing environment for Wallaby. While there are a few places you could put it, we’ll use lib/tr_web/sandbox.ex
for simplicity.
What Does the Hook Do?
Think of the hook as a helper that performs these important tasks:
- Prepares the Database: It ensures each Wallaby test starts with a fresh, isolated database, preventing conflicts.
- Connects Wallaby to Your App: The hook creates a bridge between Wallaby’s test browser sessions and your application, allowing Wallaby to interact with your code correctly. <br />
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
One of the last steps before we can run a test is to set the right case for us 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
Up next: test/test_helper.exs
:
by defining a login helper function here, you create a convenient way to simulate logged-in user sessions directly within your tests. This is crucial for testing features that require authentication.
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
Fine-tuning LiveView Tests: The Importance of async: false
Remember how we set the sandbox mode to shared in your feature case file? This setting is a smart move for test efficiency, but it has a small impact on how we write LiveView tests.
Why the Change?
Sandbox Sharing: The shared sandbox mode means multiple Wallaby tests reuse the same database environment. This is great for speed, but it requires a bit of coordination with LiveView.
Ensuring Consistency: Setting async: false
for LiveView tests helps guarantee that each LiveView test sees a consistent view of the database. This prevents unexpected outcomes that could happen with multiple concurrent LiveView tests.
use TrWeb.ConnCase, async: false
Running a test
In this example we run 2 browsers with logged in users and verify that they can send a message and receive it in real time, you will also see examples how to select and interact with different elements and run some assertions.
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
To see the results head over to the checks page here
Running it in github actions
When you want Wallaby tests to run automatically as part of your continuous integration (CI) system, there’s one important setup step: installing browser drivers.
- 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
see the full file here
The Journey Continues
This is a foundational introduction to Wallaby. In subsequent articles, we’ll explore advanced features, best practices, and ways to streamline your testing process. if you found this tutorial useful please leave a comment.
Closing notes
Let me know if there is anything that you would like to see implemented or tested, explored and what not in here…
Errata
If you spot any error or have any suggestion, please send me a message so it gets fixed.
Also, you can check the source code and changes in the sources here
-
Comments
Online: 0
Please sign in to be able to write comments.