Как писать модульные тесты с ExUnit?
В Elixir модульные тесты пишутся с использованием встроенного фреймворка ExUnit, который поставляется с языком. Он позволяет описывать, запускать и организовывать тесты в изолированных модулях. Основная цель — проверка поведения отдельных функций (юнитов), как правило, в рамках одного модуля.
Подключение ExUnit
Если ты используешь Mix-проект, то ExUnit уже подключён. В файле test/test_helper.exs обычно есть строка:
ExUnit.start()
Именно она запускает тестовую среду. Обычно все тесты находятся в папке test/.
Базовая структура теста
Каждый тест пишется в отдельном модуле, который использует ExUnit.Case. Ниже пример базового теста:
defmodule MathTest do
use ExUnit.Case
test "сложение двух чисел" do
assert 1 + 2 == 3
end
test "деление на ноль вызывает ошибку" do
assert_raise ArithmeticError, fn ->
1 / 0
end
end
end
-
test — макрос, объявляющий конкретный тест.
-
assert — проверка истинности выражения.
-
assert_raise — проверка, что определённая ошибка возникнет.
Основные утверждения (assertions)
-
assert expression — проверка, что выражение истинно.
-
refute expression — проверка, что выражение ложно.
-
assert_equal(expected, actual) — эквивалент assert expected == actual.
-
assert_in_delta(a, b, delta) — проверка на близость (например, для float).
-
assert_raise(ErrorModule, fn -> ... end) — проверка на выброс исключения.
-
assert_received(message) — для тестов с процессами и send.
Использование setup
Если требуется подготовка окружения перед запуском тестов (например, инициализация структуры), используется setup:
defmodule CalculatorTest do
use ExUnit.Case
setup do
%{a: 10, b: 20}
end
test "умножение", %{a: a, b: b} do
assert a \* b == 200
end
end
-
setup возвращает map — он автоматически подставляется в каждый test как аргумент.
-
Можно использовать setup_all, если нужно выполнить инициализацию один раз на весь модуль.
Тестирование с контекстом
Контекст (context) — это map с данными, возвращёнными из setup. Его можно использовать для передачи параметров в тесты:
setup do
user = %{id: 1, name: "Alice"}
{:ok, user: user}
end
test "проверка имени", %{user: user} do
assert user.name == "Alice"
end
Запуск тестов
Тесты можно запустить командой:
mix test
Можно запустить только конкретный файл:
mix test test/my_module_test.exs
Или даже конкретную строку:
mix test test/my_module_test.exs:42
Модули с общими функциями
Можно вынести вспомогательные функции в модуль, например:
defmodule MyApp.TestHelper do
defmacro assert_error(fun, error_module) do
quote do
assert_raise unquote(error_module), fn -> unquote(fun).() end
end
end
end
И затем подключить:
use MyApp.TestHelper
Тестирование с Mock и Stub
Elixir не имеет встроенного mocking-фреймворка, но можно использовать Mox:
defmock(MyApp.MockRepo, for: MyApp.RepoBehaviour)
setup :verify_on_exit!
test "вызов mock" do
MyApp.MockRepo
|> expect(:get, fn _ -> %{id: 1} end)
assert MyApp.MyService.get_user(1) == %{id: 1}
end
Организация папки test/
-
test/ — корень тестов
-
test/test_helper.exs — стартует ExUnit
-
test/my_module_test.exs — отдельные модули с use ExUnit.Case
-
test/support/ — хелперы, мок-данные, общие модули
Метки (теги) и фильтрация
Можно маркировать тесты тегами:
@tag :slow
test "медленный тест", \_context do
assert true
end
И запускать только определённые:
mix test --only slow
Также можно исключить:
mix test --exclude slow
Тестирование с async: true
Если модуль тестов не зависит от общего состояния (например, не работает с базой данных), можно распараллелить тесты:
use ExUnit.Case, async: true
Это ускоряет запуск.
Покрытие кода
Для анализа покрытия можно использовать mix test --cover. Также доступны библиотеки:
-
excoveralls — покрытие с HTML/JSON отчётами.
-
credo — линтер, проверка стиля.
Примеры тестов
defmodule StringTest do
use ExUnit.Case
test "length/1 возвращает количество символов" do
assert String.length("abc") == 3
end
test "split/2 разбивает строку по разделителю" do
assert String.split("a,b,c", ",") == \["a", "b", "c"\]
end
end
Модульные тесты в ExUnit ориентированы на читаемость, простоту и детерминированность. Они тесно интегрированы с экосистемой Elixir, полностью поддерживаются инструментами сборки, анализа и CI/CD.