Do you want to pick up from where you left of?
Take me there

Тестирование

Тестирование — важная часть разработки. В этом уроке мы узнаем, как тестировать наш Elixir-код с использованием ExUnit, а также познакомимся с некоторыми отличными приёмами.

ExUnit

В Elixir есть встроенный фреймворк для тестирования — ExUnit, который включает всё необходимое для тщательного тестирования нашего кода. Перед тем как двигаться дальше, стоит отметить, что тесты реализованы в виде скриптов Elixir, поэтому нам нужно использовать расширение .exs. Для того, чтобы выполнять тесты, нужно запустить ExUnit с помощью вызова ExUnit.start(), обычно это делается в test/test_helper.exs.

Когда мы сгенерировали пример проекта из прошлого урока, Mix сделал для нас тест, который можно найти в test/example_test.exs:

defmodule ExampleTest do
  use ExUnit.Case
  doctest Example

  test "greets the world" do
    assert Example.hello() == :world
  end
end

Тесты проекта можно запустить с помощью команды mix test. Если мы выполним её, то увидим примерно следующее:

..

Finished in 0.03 seconds
2 tests, 0 failures

Откуда в выводе появился второй тест? Кроме теста в test/example_test.exs, Mix также сгенерировал тест для документации в lib/example.ex.

defmodule Example do
  @moduledoc """
  Documentation for Example.
  """

  @doc """
  Hello world.

  ## Examples

      iex> Example.hello
      :world

  """
  def hello do
    :world
  end
end

assert

Если вы когда-либо ранее писали тесты, вы должны быть знакомы с assert; в некоторых фреймворках роль assert выполняют should или expect.

Макрос assert используется, чтобы проверить, что выражение истинно. В случае, если это не так, возникнет ошибка, а тесты завершатся с ошибкой. Давайте изменим наш пример и запустим mix test, чтобы протестировать ошибку:

defmodule ExampleTest do
  use ExUnit.Case
  doctest Example

  test "greets the world" do
    assert Example.hello() == :word
  end
end

Сейчас мы увидим другой результат:

  1) test greets the world (ExampleTest)
     test/example_test.exs:5
     Assertion with == failed
     code:  assert Example.hello() == :word
     left:  :world
     right: :word
     stacktrace:
       test/example_test.exs:6 (test)

.

Finished in 0.03 seconds
2 tests, 1 failures

ExUnit покажет, какое именно утверждение было ошибочным, какое значение ожидалось, и какое было получено на самом деле.

refute

Макрос refute относится к assert также, как unless к if. Используйте refute, если вы хотите убедиться, что выражение всегда ложно.

assert_raise

Иногда необходимо проверить, возникла ли в коде ошибка. Это можно сделать с помощью assert_raise. Мы столкнёмся с примером применения assert_raise в следующем уроке о Plug.

assert_receive

В Elixir приложения состоят из процессов-акторов, которые отправляют сообщения друг другу, и довольно часто нужно протестировать, что сообщения отправляются. Так как ExUnit работает в собственном процессе, он может получать сообщения. Именно получение сообщений этим процессом можно проверить с помощью assert_received:

defmodule SendingProcess do
  def run(pid) do
    send(pid, :ping)
  end
end

defmodule TestReceive do
  use ExUnit.Case

  test "receives ping" do
    SendingProcess.run(self())
    assert_received :ping
  end
end

assert_received не ждет сообщений по умолчанию, но можно указать время ожидания.

capture_io и capture_log

Получение вывода приложения возможно с использованием ExUnit.CaptureIO без изменения кода приложения. Просто передайте функцию, генерирующую вывод в качестве параметра:

defmodule OutputTest do
  use ExUnit.Case
  import ExUnit.CaptureIO

  test "outputs Hello World" do
    assert capture_io(fn -> IO.puts("Hello World") end) == "Hello World\n"
  end
end

ExUnit.CaptureLog — эквивалент отправки вывода приложения в Logger.

Настройка теста

В некоторых случаях перед тестами необходимо произвести настройку. Сделать это можно с помощью макросов setup и setup_all. Макрос setup вызывается перед каждым тестом, а setup_all — один раз перед всем набором. Ожидается, что они вернут кортеж вида {:ok, state}, state будет доступен для наших тестов.

В качестве примера изменим наш код и воспользуемся setup_all:

defmodule ExampleTest do
  use ExUnit.Case
  doctest Example

  setup_all do
    {:ok, recipient: :world}
  end

  test "greets", state do
    assert Example.hello() == state[:recipient]
  end
end

Использование заглушек

Простой совет касательно использования заглушек в Elixir: не делайте этого. Возможно, вам по привычке захочется воспользоваться заглушкой (mock), но это крайне не приветствуется сообществом Elixir по веским причинам.

Эта тема раскрыта подробнее в отличной статье. Суть в том, что вместо того, чтобы подменять методы для тестирования, создавайте интерфейсы в коде вне приложения и используйте объекты-заглушки, которые будут реализовывать этот интерфейс в процессе тестирования.

Для переключения между реализациями в коде предложения рекомендуется передавать модули в качестве аргументов функции и использовать значения по умолчанию. Если этот вариант не подходит, можно использовать встроенные механизмы конфигурации. Для создания такого подхода к заглушкам не нужна специальная библиотека. Достаточно функционала языка: поведений и функций обратного вызова.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!