VeljkoMaksimovic
Ecto.Sandbox does not work with tests that have setup_all function
Hi. I am using Ecto.Sandbox for all of my unit tests, so I’ve decided to create new ExUnit.CaseTemplate that will setup this sandbox for each test. Here is that file:
# Helper module for setting up tests that can run DB queries concurrently
# withourh interfearing with each other
# https://hexdocs.pm/ecto/testing-with-ecto.html
defmodule Guard.RepoCase do
use ExUnit.CaseTemplate
using do
quote do
import Guard.RepoCase
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Guard.Repo)
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Guard.FrontRepo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Guard.Repo, {:shared, self()})
Ecto.Adapters.SQL.Sandbox.mode(Guard.FrontRepo, {:shared, self()})
end
:ok
end
end
Now this is how one of my Test modules looks, where above mentioned RepoCase with Ecto.Sandbox is used:
defmodule Guard.Store.RbacRole.Test do
use Guard.RepoCase, async: true
@user_id "cb358a11-4185-4b5b-8829-5619805ac1fe"
setup_all do
Support.Factories.RbacUser.insert_user_into_db(@user_id)
{:ok, %{}}
end
Support.Factories.RbacUser.insert_user_into_db is literally just one Ecto.insert call that creates user with the given id, nothing special. This user needs to be in database for each test within this module, and that is why I decided to use setup_all instead of setup.
BUT, whenever I run tests like this, I get this error:
** (DBConnection.ConnectionError) connection not available and request was dropped from queue after 2623ms. This means requests are coming in and your connection pool cannot serve them fast enough. You can address this by:
1. Ensuring your database is available and that you can connect to it
2. Tracking down slow queries and making sure they are running fast enough
3. Increasing the pool_size (albeit it increases resource consumption)
4. Allowing requests to wait longer by increasing :queue_target and :queue_interval
See DBConnection.start_link/2 for more information
stacktrace:
(db_connection 2.4.0) lib/db_connection/ownership.ex:95: DBConnection.Ownership.ownership_checkout/2
(ecto_sql 3.7.0) lib/ecto/adapters/sql/sandbox.ex:499: Ecto.Adapters.SQL.Sandbox.checkout/2
(guard 0.1.0) test/support/concurrent_repo_case.ex:15: Guard.RepoCase.__ex_unit_setup_0/1
(guard 0.1.0) test/support/concurrent_repo_case.ex:4: Guard.RepoCase.__ex_unit__/2
test/guard/store/rbac_role_test.exs:1: Guard.Store.RbacRole.Test.__ex_unit__/2
The problem is (at least I think) that setup_all from my Test module is called before setup function where Sandbox is initialized, and that creates some sort of problem.
If I change setup_all within my Test module to setup, which looks like this:
setup do
Support.Factories.RbacUser.insert_user_into_db(@user_id)
{:ok, %{}}
end
everything works just fine.
Im not satisfied because now I have to insert this user before each test, which is ~15 db insert queries, instead of just one.
Does anyone have any idea if it is possible to run setup_all method just once, but to ‘delay’ execution of that method for after ecto.Sandbox has been already initialized. Thanks a lot ![]()
Most Liked
trisolaran
Right, the idea of using the sandbox for tests is that each test has to checkout a connection at the beginning of the test, the connection is wrapped in a transaction which is then rolled back at the end of the test. So each test can work with its own data without interfering with other tests (with some caveats). The connection is checked out in the setup callback, which is called once for each test in the case. The setup_all callback is called once for the entire case, and it’s called before any setup call and therefore before any connection is checked out.
The sandbox connection is checked out once for every test, so “delaying” the execution of your setup_all logic until then would mean running it once for each test
it would be just like adding that logic to the setup call.
I personally wouldn’t worry about it. Your test case is async so will run in parallel with other tests. The benefits of having isolated tests, each with their own test data are IMO greater then the performance gains of inserting the test data once for all tests. At least in the general case.
However, if you think that you’d benefit from having tests share some data, you could seed the test database with the appropriate data before running the test suite (of course, tests shouldn’t modify that data, otherwise they may fail in funny ways). See this thread: Fixture on tests load once and keep for all the tests
LostKobrakai
setup_all also runs in a different process than the test itself, while setup runs within the test’s process. Connections and any transactions opened cannot be shared between multiple processes, so setup_all can’t be used with the ecto sandbox no matter the timing/order of execution.
LostKobrakai
That kinda blew my mind shortly, because you usually don’t have an API to do so, but true the sandbox actually does that.
Last Post!
ooddaa
I have run into the same problem today. Here’s how I solved it.
in data_case.exs
defmodule MyModule.DataCase do
@moduledoc """
Sets up helpers for CRUD tests.
In test file do:
# this will set up a shared sandbox connection for all tests
use MyModule.DataCase, async: true, shared_conn: true
# each test will check it out before running
setup [:checkout_repo_conn]
# make specific test setup for all tests to share.
# we create a fresh job,
# which will persist for all tests in the suite and
# after they finis, the transaction will be rolled back.
setup_all do
{:ok, job} = Job.create(Factory.params_for(:job))
%{job: job}
end
# provide each test with the fresh job to use.
test "blabbla", %{job: job} do...
"""
use ExUnit.CaseTemplate
using options do
quote do
alias Ecto.Changeset
import MyModule.DataCase
alias MyModule.{Factory, Repo}
defp checkout_repo_conn(_context) do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
end
setup_all do
if unquote(options)[:shared_conn] == true do
Ecto.Adapters.SQL.Sandbox.mode(MyModule.Repo, {:shared, self()})
:ok
else
:ok = Ecto.Adapters.SQL.Sandbox.mode(MyModule.Repo, :manual)
end
end
end
end
end
in test.exs
defmodule MyModule.Test do
use MyModule.DataCase,
async: true,
shared_conn: true # make it shared
alias MyModule.{Repo, JobDetail, Job}
# each test will checkout a sandbox connection, which will be shared
setup [:checkout_repo_conn]
# here I set up shared context to run once
setup_all do
{:ok, job} = Job.create(Factory.params_for(:job))
%{job: job}
end
# and provide the job to each test to use its job.id
test "blabla", %{job: job} do...
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









