Как писать модульные тесты с 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.