Understanding Test Concurrency In Elixir のブログ記事を読む
dockyardのブログが勉強になるのでメモがてらscrap化
テストの実行順
- test_helper.exsが実行される
- async: true とmarkしたテストモジュールが実行される
- async: false とmarkしたテストモジュールが実行される
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.
ということは、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
Ecto.Adapters.SQL.Sandbox
の話が続く。checkout/1
を実行するとデフォルトで sandbox: true
としてコネクションが確立される。テスト用のtransactionが張られ、テスト実行後にrollbackが自動で行われるため、他のテストに影響を与えずにテストケース毎に独立してテストを実行できる。
これはphoenixでプロジェクト作ったら意識せずとも conn_case.ex
や data_case.ex
で上手いことやってくれてるところ。
確立されたコネクションを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.
shared modeの話はhexdocsの方がわかりやすいかも
別プロセスでDB接続を共有して非同期に時間がかかる処理をさせるなどして、checkoutしたテストプロセスが先に終了した場合、"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.