Что такое mock и как его сделать в Elixir?
В Elixir mock — это подставной (временный, подменный) модуль или функция, используемая в тестах для имитации поведения внешних зависимостей: API, базы данных, сторонних библиотек, процессов и т.п. Это особенно важно при юнит-тестировании, когда необходимо протестировать модуль изолированно от его окружения.
Elixir не имеет встроенного механизма мокирования как в некоторых ООП-языках (Java, Python), но предоставляет гибкие средства через контрактные интерфейсы и библиотеку Mox, которая официально поддерживается командой Elixir.
Как устроено мокирование в Elixir
Мокирование строится на поведенческом контракте (behaviour) и внедрении зависимости через параметры или конфигурацию.
1. Определение behaviour (контракта)
defmodule MyApp.ExternalAPI do
@callback get_data(String.t()) :: {:ok, any()} | {:error, String.t()}
end
Это определение интерфейса, который должны реализовать как настоящий, так и подставной модуль.
2. Реализация настоящего модуля
defmodule MyApp.RealAPI do
@behaviour MyApp.ExternalAPI
def get_data(id) do
\# настоящий вызов API
{:ok, "данные из API для #{id}"}
end
end
3. Добавление зависимости через конфигурацию
\# config/config.exs
config :my_app, :external_api, MyApp.RealAPI
4. Использование зависимости в коде
defmodule MyApp.DataService do
def fetch(id) do
api = Application.get_env(:my_app, :external_api)
api.get_data(id)
end
end
5. Установка Mox
Добавьте зависимость в mix.exs:
def deps do
\[
{:mox, "~> 1.0", only: :test}
\]
end
После этого запустите mix deps.get.
6. Определение мока
\# test/support/mocks.ex
Mox.defmock(MyApp.MockAPI, for: MyApp.ExternalAPI)
И подключите файл в test/test_helper.exs:
ExUnit.start()
Mox.Server.start_link(\[\])
Code.require_file("support/mocks.ex", \__DIR_\_)
7. Конфигурация мока в тесте
use ExUnit.Case, async: true
setup :set_mox_global
setup :verify_on_exit!
test "обрабатывает ответ от мока" do
MyApp.MockAPI
|> expect(:get_data, fn "123" -> {:ok, "тестовые данные"} end)
Application.put_env(:my_app, :external_api, MyApp.MockAPI)
assert MyApp.DataService.fetch("123") == {:ok, "тестовые данные"}
end
Ключевые особенности Mox
-
expect(module, fun_name, arity, fun) — задаёт поведение мока.
-
Все вызовы проверяются, и если они не произошли — тест падает.
-
По умолчанию моки работают только в одном процессе — set_mox_global позволяет использовать их в async: true тестах.
Альтернатива: ручной мок без Mox
Если не используется Mox, можно просто передать модуль как параметр:
def fetch(id, api \\\\ MyApp.RealAPI) do
api.get_data(id)
end
В тесте:
defmodule FakeAPI do
def get_data(\_id), do: {:ok, "фейковые данные"}
end
test "тест с ручным моком" do
assert MyApp.DataService.fetch("42", FakeAPI) == {:ok, "фейковые данные"}
end
Этот подход прост и не требует зависимостей, но не проверяет количество вызовов или аргументы.
Использование Mox.expect/4 и Mox.stub_with/2
Можно задать стандартное поведение с помощью stub_with:
Mox.stub_with(MyApp.MockAPI, MyApp.RealAPI)
А затем выборочно expect нужные вызовы.
Пример с несколькими вызовами
MyApp.MockAPI
|> expect(:get_data, fn "id1" -> {:ok, "one"} end)
|> expect(:get_data, fn "id2" -> {:ok, "two"} end)
Можно использовать expect/3 с числом вызовов:
expect(MyApp.MockAPI, :get_data, 2, fn _ -> {:ok, "result"} end)
Моки в Elixir реализуются через контракты и подмену модулей, благодаря чему достигается явное управление зависимостями, тестируемость и чистота кода. Использование Mox позволяет гарантировать, что все вызовы действительно произошли, и упрощает поддержку крупных проектов.