How to test code with async functions?

I am trying to write tests for some of functions containing Task.async codes and it keep throwing errors at me. I don’t believe I am touch anything related to process ownership and have no idea what the error is about.

defmodule MyApp.Test do
  @docmoudle """
  Dummy module
  """
  alias MyApp.{Config, Repo}

  def test() do
    Repo.all(Config)
  end
end
defmodule MyApp.TestTest do
  use MyApp.ConnCase, async: true
  use Plug.Test

  test "test a test" do
    task =
      Task.async(fn ->
        MyApp.Test.test()
      end)

    # It failed on anything related to the async `task`
    # any of these two lines will throw an error
    Task.yield(task)
    IO.inspect(task)

  end
end

The long error is like

[error] Task #PID<0.521.0> started from #PID<0.519.0> terminating
** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.521.0>.

When using ownership, you must manage connections in one
of the four ways:

* By explicitly checking out a connection
* By explicitly allowing a spawned process
* By running the pool in shared mode
* By using :caller option with allowed process

The first two options require every new process to explicitly
check a connection out or be allowed by calling checkout or
allow respectively.

The third option requires a {:shared, pid} mode to be set.
If using shared mode in tests, make sure your tests are not
async.

The fourth option requires [caller: pid] to be used when
checking out a connection from the pool. The caller process
should already be allowed on a connection.

If you are reading this error, it means you have not done one
of the steps above or that the owner process has crashed.
1 Like

This is an Ecto SQL Sandbox error message. You are using the ownership system. Read more about it here:

Basically, this is whats occurring in the test you wrote.

  • The test process owns the database connection spawns a new task without granting permission for that task to use the database connection.
  • The spawned task then immediately attempts to use the database (probably before you even hit the Task.yield/1 call.

This issue usually occurs with the Task.async/1 living within the source code to be tested, not the test code. This is easier to fix within the test code itself, you’ll just have to make your process wait until its been granted access or explicitly allow it within the spawned process. In the above article you’ll see a mention of this being in your production code (which you could just put into your test code instead)

parent = self()
task = Task.async(fn ->
  if Mix.env == :test do
    Ecto.Adapters.SQL.Sandbox.allow(Repo, parent, self())
  end
  # ... child code ...
end)
1 Like

Thank you very much.
I guess I have to use the :share mode too. All of my Taks.aysnc functions are actually in production code base. The example above is just for illustration purpose.

It is a pity that all of the tests are no longer running in parallel anymore.