Что такое doctest и как его использовать?

В Elixir doctest — это механизм тестирования, позволяющий писать тесты прямо в документации к функциям. Эти тесты извлекаются из комментариев и автоматически выполняются при запуске тестов. Это удобно для демонстрации использования функции и для одновременной проверки корректности её работы.

Как работает doctest

Elixir позволяет добавлять документацию к функциям с помощью макроса @doc. Внутри можно вставлять интерактивные примеры кода, начиная со строки iex>. Когда вы используете doctest, эти примеры интерпретируются как настоящие тесты и выполняются с помощью ExUnit.

Пример: простая функция с doctest

Файл lib/math.ex:

defmodule Math do

@moduledoc """

Простейшие арифметические функции.

"""

@doc """

Складывает два числа.

## Примеры

iex> Math.sum(1, 2)
3
iex> Math.sum(-5, 5)
0
"""
def sum(a, b), do: a + b
end

Затем в test/math_test.exs:

defmodule MathTest do
use ExUnit.Case
doctest Math
end

Теперь при запуске mix test все iex> строки в @doc будут интерпретироваться как реальные тесты.

Формат написания примеров

@doc """

Возвращает квадрат числа.

## Примеры

iex> MyMath.square(4)
16
iex> MyMath.square(-3)
9
"""
def square(x), do: x \* x

— Всё, что начинается с iex> — это выражение;
— Следующая строка — ожидаемый результат;
— Можно использовать несколько выражений подряд;
— Также поддерживается вывод ошибок.

Пример с ошибкой

@doc """

Делит число на другое.

## Примеры

iex> MyMath.divide(10, 2)
5.0
iex> MyMath.divide(10, 0)
\*\* (ArithmeticError) bad argument in arithmetic expression
"""
def divide(a, b), do: a / b

Doctest проверит, что при делении на ноль будет выброшено именно это исключение с ожидаемым сообщением.

Требования и ограничения

  1. iex> должен находиться в начале строки.

  2. В одном @doc можно писать несколько примеров.

  3. Doctest не должен зависеть от состояния, внешних ресурсов или IO.

  4. Пример должен быть полностью детерминированным.

  5. Если вы меняете сигнатуру функции, не забудьте обновить примеры.

Структура проекта с doctest

  • lib/my_module.ex — основной код с документацией и doctest.

  • test/my_module_test.exs — подключает doctest MyModule.

Уточнение, как работает doctest

Когда запускается doctest MyModule, компилятор анализирует @moduledoc и @doc в MyModule и вытаскивает все строки с iex>, превращая их во внутренние вызовы test.

На низком уровне это трансформируется примерно так:

test "doctest for sum/2" do
assert Math.sum(1, 2) == 3
end

Советы по использованию

  • Делайте doctest только для простых и предсказуемых функций.

  • Используйте doctest в библиотеках, чтобы пользователи видели сразу пример использования.

  • Не пишите в doctest вызовов, зависящих от времени, случайных значений или сетевых ресурсов.

Как отключить отдельный doctest

Иногда не хочется включать doctest для конкретной функции. Для этого используется атрибут:

@doc false
def internal_function, do: ...

Или можно использовать фильтры через :except:

doctest MyModule, except: \[:moduledoc\]

Также можно указать only: [...], если нужно тестировать только определённые функции.

Использование в библиотеке

Если ты пишешь библиотеку и хочешь автоматически протестировать примеры из документации — doctest идеален. Он обеспечивает:

  • Синхронизацию кода и документации;

  • Надёжность и доверие к doc-примерам;

  • Проверку ошибок и исключений.

Комбинирование с обычными тестами

Ты можешь совмещать doctest и test в одном файле:

defmodule MyModuleTest do
use ExUnit.Case
doctest MyModule
test "ручной тест" do
assert MyModule.func() == :ok
end
end

Doctest в Elixir делает документацию живой, проверяемой и синхронизированной с реальным кодом, а сам механизм является частью стандартной библиотеки и работает без дополнительных зависимостей.