Closed8

Understanding Test Concurrency In Elixir のブログ記事を読む

koga1020koga1020

テストの実行順

  1. test_helper.exsが実行される
  2. async: true とmarkしたテストモジュールが実行される
  3. async: false とmarkしたテストモジュールが実行される

test concurrency

koga1020koga1020

async: trueとしたモジュール同士は非同期に実行されるけど、1テストモジュール内のtest caseたちはserialに実行される というのがポイント

Next, using one process per test, ExUnit runs the tests for all modules marked async: true. The tests in one async module will run concurrently with the tests in another async module. However, tests within the same test module run serially.

This fact is worth highlighting: tests within the same test module always run one at a time, even if that module is marked async.

koga1020koga1020

ということは、asnyc: trueとしているmoduleを細かく分割すれば、テストの並行性を高めることができると。高速化が期待できそう。

Breaking apart a large test module into smaller ones will automatically increase test concurrency. They could even be submodules within the same file:

defmodule MyApp.MathTest do

  defmodule AdditionTests do
    use ExUnit.Case, async: true

    test "it adds" do
      # ...
    end
  end

  defmodule SubtractionTests do
    use ExUnit.Case, async: true

    test "it subtracts" do
      # ...
    end
  end
end
koga1020koga1020

Ecto.Adapters.SQL.Sandbox の話が続く。checkout/1 を実行するとデフォルトで sandbox: true としてコネクションが確立される。テスト用のtransactionが張られ、テスト実行後にrollbackが自動で行われるため、他のテストに影響を与えずにテストケース毎に独立してテストを実行できる。

これはphoenixでプロジェクト作ったら意識せずとも conn_case.exdata_case.ex で上手いことやってくれてるところ。

koga1020koga1020

確立されたコネクションをshareする方法が2パターンある

Once a test process has a connection, there are a couple of ways it can share it. The first is to remain in :manual mode and use allow/3, which says “make the connection specified for this repo and owned by process A accessible to process B.” Taking an example straight from the documentation:

Ecto.Adapters.SQL.Sandbox.allow/3 を実行して明示的に許可

test "create two posts, one sync, another async" do
  parent = self()
  task = Task.async(fn ->
    Ecto.Adapters.SQL.Sandbox.allow(Repo, parent, self())
    Repo.insert!(%Post{title: "async"})
  end)
  assert %Post{} = Repo.insert!(%Post{title: "sync"})
  assert %Post{} = Task.await(task)
end

Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, pid}) を実行して sharedモードとして実行する

  :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
  Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})

②のshared modeで実行するということは、テストは同時実行できないことを意味する。
すなわち、shared modeはasync: falseの場合のみ設定できる

So any test using :shared mode must be in a test module marked async: false. This means no tests from other modules will run concurrently with it. Since tests in the same module run serially, it will be the only test running until it completes and may share its database connection fearlessly with the processes whose behavior it tests.

koga1020koga1020

別プロセスでDB接続を共有して非同期に時間がかかる処理をさせるなどして、checkoutしたテストプロセスが先に終了した場合、"owner exited" エラーが発生するので注意。

https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html#module-owner-exited

test "gets results from GenServer" do
  {:ok, pid} = MyAppServer.start_link()
  Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
  assert MyAppServer.get_my_data_fast(timeout: 1000) == [...]
end

To understand the failure, we need to answer the question: who are the owner and client processes? The owner process is the one that checks out the connection, which, in the majority of cases, is the test process, the one running your tests. In other words, the error happens because the test process has finished, either because the test succeeded or because it failed, while the client process was trying to get information from the database. Since the owner process, the one that owns the connection, no longer exists, Ecto will check the connection back in and notify the client process using the connection that the connection owner is no longer available.

このスクラップは2022/02/11にクローズされました