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.

Last Post!

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... 

Where Next?

Popular in Questions Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
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
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement