-
Notifications
You must be signed in to change notification settings - Fork 347
Cookbook: Concurrent, transactional tests with Phoenix.Ecto.SQL.Sandbox
agatheblues edited this page Apr 5, 2022
·
11 revisions
The following Tesla middleware allows to run asynchronous, transactional tests on an API endpoint for applications using Ecto's SQL.Sandbox
.
- you are developing an application that uses Ecto for database access,
- and you have some kind of HTTP API that you'd like to use to drive an integration test suite (using Tesla as API client),
- you would like to be able to use asynchronous, transactional tests using Ecto's
Ecto.Adapters.SQL.Sandbox
in:manual
mode
- Ecto's sandbox wraps your test logic in a database transaction and automatically rolls it back when the tests completes, so tests are isolated from each other and any changes made to the database are undone.
-
:manual
mode requires processes to explicitly "checkout" a sandboxed DB connection before executing a SQL statement. This is usually done in asetup
block in your test case templates -- see for instance the usualDataCase
template generated by Phoenix.-
:manual
mode has the advantage that it allows concurrent tests to each have their own isolated transaction - However, SQL statements executed in processes that did not checkout a sandboxed connection will receive an error. Hence, your test logic must not trigger code that does not run within the same process -- for instance it may not call an HTTP endpoint where each request is handled by a new throw-away process, e.g. an API or web endpoint.
- To overcome this "limitation", one can make a separate process "join" a sandboxed connection. It can explicitly request shared ownership from Ecto's sandbox, given it knows the
pid
(and some other metadata) of the process that originally initiated the transaction (i.e., the test process).
-
- Sharing this information is often quite tricky, depending on the structure of the code involved. The
Phoenix.Ecto.SQL.Sandbox
plug from thephoenix_ecto
package neatly solves this problem for Phoenix endpoints: It tries to extract the sandbox metadata from an HTTP header -- defaulting to theuser-agent
-- and requests shared ownership for the process handling the HTTP request. Any test process exercising the given endpoint can use the HTTP header to store its sandbox information and thus extend its sandbox to the request process.- Some libraries (e.g. wallaby) have built-in logic to store the sandbox metadata in the
user-agent
field. - The code below provides this functionality as a Tesla middleware.
- Some libraries (e.g. wallaby) have built-in logic to store the sandbox metadata in the
- Set up
Phoenix.Ecto.SQL.Sandbox
as described in its documentation. - Import the Tesla middleware below into your codebase -- e.g. somewhere in
test/support
. - Make your tests use a Tesla client configured to use the middleware. Please note that the code below is disabled if the
:sql_sandbox
config variable isn't set. This is useful if your "test client" is also used in production. If your Tesla client is exclusively used for tests, you can remove the conditional.
defmodule MyApp.TeslaSQLSandboxMiddleware do
@moduledoc false
@behaviour Tesla.Middleware
if Application.compile_env!(:my_app, :sql_sandbox) do
@impl Tesla.Middleware
def call(env, next, _opts) do
user_agent = Tesla.get_header(env, "user-agent")
encoded_metadata =
MyApp.Repo
|> Phoenix.Ecto.SQL.Sandbox.metadata_for(self())
|> Phoenix.Ecto.SQL.Sandbox.encode_metadata()
env
|> Tesla.put_header("user-agent", "#{user_agent}/#{encoded_metadata}")
|> Tesla.run(next)
end
else
@impl Tesla.Middleware
def call(env, next, _opts) do
Tesla.run(env, next)
end
end
end