Как тестировать асинхронный код?
В Elixir тестирование асинхронного кода выполняется с помощью модуля ExUnit, который входит в стандартную библиотеку. Он предоставляет инструменты для управления временем, проверки сообщений между процессами и отслеживания завершения задач. Тестирование асинхронного кода в функциональной и конкурентной среде Elixir требует понимания процессов, сообщений и состояния.
Основные подходы к тестированию асинхронного кода
1. Тестирование сообщений между процессами
Если ваш код использует send/receive, вы можете проверить, было ли сообщение отправлено нужному процессу:
test "отправка сообщения" do
send(self(), :hello)
assert_receive :hello
end
-
send(self(), ...) — имитация отправки сообщения текущему процессу.
-
assert_receive — проверяет, пришло ли сообщение за 100 мс (по умолчанию).
-
Если нужно больше времени: assert_receive :msg, 1000
2. Тестирование с использованием Task
Если используется Task.async/await, то просто вызываем await:
test "асинхронная задача" do
task = Task.async(fn -> 1 + 2 end)
result = Task.await(task)
assert result == 3
end
— Task.await блокирует выполнение до завершения задачи (или выхода по таймауту).
Если задача длительная, можно настроить таймаут:
Task.await(task, 5000) # ждём до 5 секунд
3. Тестирование GenServer
Если ваш модуль — GenServer, можно напрямую вызывать его интерфейс:
test "GenServer ответ" do
{:ok, pid} = MyServer.start_link(\[\])
MyServer.set_value(pid, 42)
assert MyServer.get_value(pid) == 42
end
Иногда нужно проверить внутреннее состояние:
state = :sys.get_state(pid)
assert state == %{value: 42}
— Только для отладки и тестов. В проде лучше не использовать :sys.
4. Проверка send в GenServer
Если GenServer отправляет сообщение в другой процесс:
def handle_cast({:notify, msg}, state) do
send(state.pid, msg)
{:noreply, state}
end
В тесте:
test "уведомление" do
{:ok, pid} = MyServer.start_link(pid: self())
MyServer.notify(:ping)
assert_receive :ping
end
5. Использование setup для подготовки окружения
Если каждый тест должен запускать отдельный процесс, используем setup:
setup do
{:ok, pid} = MyGenServer.start_link(\[\])
%{pid: pid}
end
test "значение сохраняется", %{pid: pid} do
MyGenServer.set(pid, 10)
assert MyGenServer.get(pid) == 10
end
6. Контроль времени с помощью :timer и Process.sleep
Иногда в тесте нужно подождать асинхронную операцию:
test "сообщение придет через 50 мс" do
spawn(fn ->
:timer.sleep(50)
send(self(), :done)
end)
assert_receive :done, 100
end
— assert_receive автоматически ждёт.
— Process.sleep/1 используется редко в тестах, но допустимо при необходимости.
7. Использование capture_log для перехвата логов
Если ваш код логирует асинхронные ошибки, можно проверить логи:
import ExUnit.CaptureLog
test "ошибка логируется" do
log =
capture_log(fn ->
MyModule.do_something_wrong()
end)
assert log =~ "ошибка"
end
8. Контроль порядка выполнения
Если несколько сообщений приходят в разном порядке — используйте assert_receive поочерёдно. Или проверяйте содержимое Mailbox вручную, используя flush.
Пример:
flush_messages = fn ->
receive do
msg -> \[msg | flush_messages.()\]
after
0 -> \[\]
end
end
9. Тестирование процессов, которые могут упасть
Вы можете использовать Process.flag(:trap_exit, true) в тесте, если проверяете выход процесса:
test "процесс завершился" do
Process.flag(:trap_exit, true)
pid = spawn_link(fn -> exit(:normal) end)
assert_receive {:EXIT, ^pid, :normal}
end
Это полезно при тестировании Supervisor'ов и мониторинга.
10. Именованные процессы
Удобно использовать имена процессов для взаимодействия в тестах:
MyServer.start_link(name: :my_server)
MyServer.set_value(:my_server, 5)
assert MyServer.get_value(:my_server) == 5
11. Тестирование с Mox (если есть зависимости)
Если процесс зависит от других модулей (например, внешнего API), используйте Mox для мокирования:
Mox.expect(MyAPI.Mock, :get_data, fn -> {:ok, "response"} end)
12. Тестирование в асинхронном режиме
Модуль ExUnit позволяет запускать тесты параллельно, если они независимы:
ExUnit.start(async: true)
И в файле test/my_test.exs:
defmodule MyTest do
use ExUnit.Case, async: true
end
— Но нельзя использовать общие ресурсы или глобальные имена, иначе получишь race condition.
Асинхронный код в Elixir полностью тестируем за счёт процессов, сообщений, замыканий и инструментов типа Task, assert_receive, capture_log, Mox, ExUnit.Case, setup, spawn, Process.flag и других.