VeljkoMaksimovic

VeljkoMaksimovic

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:

Showing Posts 1 to 9

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.

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.

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.

VeljkoMaksimovic

VeljkoMaksimovic OP

@LostKobrakai @trisolaran Thanks for help guys.

al2o3cr

al2o3cr

One situation when you do want this is if RbacUser has a unique index on that user ID - inserting that record in a setup block (inside a transaction) will block any other test that also inserts that record in a setup block, causing mayhem with async: true tests.

I’m not sure setup_all blocks run early enough for that, though. I’ve used code in test_helper.ex to insert records before the sandbox is started:

# generated by Phoenix
ExUnit.start()

# add insert statements here

# generated by Phoenix
Ecto.Adapters.SQL.Sandbox.mode(YourAppNameHere.Repo, :manual)

The other side-effect of this approach is that the test database has records in it at the end, since those initial inserts aren’t inside a transaction. You can either live with those (and change the inserts to be find-or-inserts) or reset the DB on every test run.

VeljkoMaksimovic

VeljkoMaksimovic OP

You raised a good point with unique indexes, and that is the case here. Tests work fine, but I assume that parallelization thats enabled with Ecto.Sandbox and tests running in separate processes is reduced here since those separate processes cant write to DB simultaneously.

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
ooddaa

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... 
— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
matt-savvy
Anyone here using Honeybadger? My Honeybadger account is being overwhelmed with noise from some bots. Seeing a lot of Bandit.HTTPError...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews