VeljkoMaksimovic

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 :slight_smile:

Most Liked

trisolaran

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 :slight_smile: 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

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

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.

trisolaran

trisolaran

Correct. If you have 2 parallel tests A and B that both insert the same value in a column with unique index as part of their setup, and A runs first, then B will have to wait until A completes. Because each transaction is rolled back after the test ends, there will be no conflicts (unless you create a deadlock) and the tests will succeed, but they won’t really run in parallel. I was following this naive approach for quite some time until someone made me realize that it wasn’t a good idea :slight_smile:

One way to solve this is to have A and B insert distinct, dynamic values and not hard-coded ones. For example this is what I typically do:

setup do 
 # Fixture.create_user/0 creates a new user with a randomly generated ID, or a new ID created in the DB
 user = Fixture.create_user()

 %{user: user}
end

test "can get user by id", %{user: user} do 
  assert User.get(user.id) == user
end
trisolaran

trisolaran

They can, that’s why we have allowances and the sanbox shared mode.

But yes, the fact that setup_all runs in a separate process before all tests is another reason why the idea of postponing its execution doesn’t make much sense.

Where Next?

Popular in Questions Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

Other popular topics Top

joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New

We're in Beta

About us Mission Statement