Все, что вам нужно знать, чтобы начать тестирование приложений Phoenix

Я до сих пор помню самые ранние дни моей инженерной карьеры. Я был студентом третьего курса университета и только что начал стажировку в крупной компании-разработчике программного обеспечения.

Это было захватывающее время в моей жизни. У меня впервые появилась возможность внести свой вклад в производственную кодовую базу. Я научился разрабатывать и выпускать функции программного обеспечения. В целом, я подобрал множество передовых практик, которые помогли мне начать разработку программного обеспечения.

К сожалению, одной практикой, которая не привлекла мое внимание в то время, было тестирование. Так я стал тем разработчиком, который по возможности избегает написания тестов.

Долгое время мне очень удавалось уклоняться от ответственности за написание тестов. Так продолжалось до тех пор, пока один из моих коллег по Mindvalley не обратил внимание на мое поведение. Затем он привел очень убедительный аргумент в пользу того, что разработчикам всегда следует заботиться о написании тестов.

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

И действительно, мой коллега был прав. Написание тестов оказалось проще, чем я думал, и фактически повысило мою продуктивность как разработчика.

Итак, в сегодняшней статье я хочу поделиться с вами своими знаниями. Я расскажу, что такое тестирование в целом, и покажу вам, что вам нужно знать, чтобы начать писать тесты на Эликсире.

Основы

Прежде всего, хочу кое-что уточнить. Разработчики могут реализовать множество типов тестов. Сегодняшняя запись в блоге будет посвящена модульному тестированию в Elixir / Phoenix.

Типичное веб-приложение имеет следующие характеристики:

  • Это набор модулей.
  • Каждый модуль представляет собой набор функций.
  • Различные модули взаимодействуют друг с другом, вызывая функции друг друга.

Каждая функция данного модуля имеет следующие характеристики:

  • Он принимает определенное количество входных параметров.
  • Он выполняет ряд операций с этими входами.
  • Он завершает свою задачу, возвращая единственный результат.

А теперь давайте свяжем все это вместе, изучив следующее определение из Википедии:

Модульные тесты обычно представляют собой автоматизированные тесты, написанные и выполняемые разработчиками программного обеспечения, чтобы гарантировать, что часть приложения (известная как «модуль») соответствует своему дизайну и ведет себя так, как задумано. В процедурном программировании единицей может быть целый модуль, но чаще это отдельная функция или процедура.

Другими словами, модульное тестирование - это написание кода, который выполняет следующие действия:

  • устанавливает значения входных параметров
  • вызывает функцию с указанными входными значениями
  • проверяет вывод функции

В зависимости от входных значений одна функция может иметь несколько модульных тестов.

Написание модульных тестов - это не ракетостроение, но для этого требуется определенная настройка. В большинстве современных языков программирования есть библиотеки, которые помогают разработчикам легко реализовывать тестовые примеры. К счастью для нас, Elixir не является исключением из этого правила.

Библиотеки эликсира для тестирования

Одна из вещей, которые мне нравятся в сообществе Elixir, - это его стремление создавать исчерпывающую и легкую для понимания документацию.

Вот почему в сегодняшней статье я планирую сделать свои объяснения краткими и простыми. И вместо этого сосредоточьтесь на обмене примерами кодирования и ссылках на наиболее релевантные библиотечные документы, которые вам нужно пройти.

ExUnit

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

  • ExUnit.Case - содержит помощники для определения тестовых случаев (описать, проверить и т. Д.)
  • ExUnit.Assertions - содержит набор функций утверждения, которые по умолчанию импортируются в ваши тестовые примеры (assert, refute и т. Д.)
  • ExUnit.Callbacks - определяет обратные вызовы ExUnit. Эти функции позволяют разработчикам реализовывать фикстуры и писать любой код предварительной инициализации (setup, setup_all, on_exit и т. Д.)
  • ExUnit.DocTest - супер крутой модуль, который умеет извлекать тест-кейсы на основе вашей документации.

Как я уже упоминал ранее, ExUnit - это основная библиотека тестирования Эликсира. Так что будет лучше, если вы будете чувствовать себя комфортно с ним. Когда дело доходит до написания тестов, это единственная библиотека, которую вы должны знать наизусть.

Mox

Следующим в моем списке библиотек для тестирования Эликсира стоит Mox. Это позволяет разработчикам создавать и работать с параллельными макетами.

Предположим, что тестируемая функция имеет внешнюю зависимость. Функция A1, расположенная в модуле A, должна вызывать функцию B1, расположенную в модуле B. Вы можете использовать библиотеку Mox, чтобы заменить фактический вызов функции на предустановленное значение ответа.

Если это объяснение звучит нечетко, не волнуйтесь. Мы немного рассмотрим практический пример того, как работает Mox.

Ex_Machina

Ex_Machina - это библиотека, разработанная ThoughtBot. Он отвечает за создание тестовых данных и ассоциаций. Ex_Machina имеет фантастический набор пользовательских документов, поэтому я предлагаю вам найти время и изучить их.

mix_test_watch

На мой взгляд, одна из самых полезных публично доступных библиотек Elixir. У него простая задача - автоматически повторно запускать тесты вашего проекта Elixir при сохранении файла. Это идеальный инструмент для разработки, основанной на тестировании. Вот ссылка на репозиторий Github: https://github.com/lpil/mix-test.watch

Хорошо, зная об этих четырех библиотеках, теперь мы можем взглянуть на некоторый код.

Написание примеров модульных тестов

Мы можем начать с создания модуля Demo.Greetings, который реализует одну функцию - say_hello/1.

# greetings.ex
defmodule Demo.Greetings do
  def say_hello(name) when is_binary(name) do
    "Hello, #{name}!" 
  end
  def say_hello(_), do: "Hello!"
end

На основе кода в Demo.Greetings мы сделаем следующее:

  • создайте новый модуль с именем Demo.GreetingsTest и добавьте к нему два новых тестовых случая
  • в обоих случаях проверяется поведение функции say_hello/1
  • Для написания тестов мы будем полагаться на библиотеку ExUnit и ее модули - ExUnit.Case и ExUnit.Assertions.
# greetings_test.exs
defmodule Demo.GreetingsTest do
  use ExUnit.Case, async: true
  alias Demo.Greetings
  describe "say_hello/1" do
    test "return greeting when name is not a string" do
      assert Greetings.say_hello(nil) == "Hello!"
    end
    test "return greeting when name is string" do
      name = "Velina"
      assert Greetings.say_hello(name) == "Hello, Velina!"
    end
  end
end

Сам код довольно прост и не требует пояснений. Мы устанавливаем наши входные значения; мы вызываем функцию say_hello/1 и проверяем ее ответ.

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

Теперь давайте внесем некоторые улучшения в наш Demo.Greetings модуль. Мы передадим в нашу say_hello функцию дополнительный параметр language и получим локализованное приветственное сообщение.

Мы реализуем логику получения переведенного сообщения в отдельный модуль.

# greetings.ex
defmodule Demo.Greetings do
  alias Demo.LocalisedGreetings
  def say_hello(name, language) when is_binary(name) do
    {:ok, greeting} = LocalisedGreetings.get_hello(language)
    "#{greeting}, #{name}!" 
  end
  def say_hello(_, language) do
    {:ok, greeting} = LocalisedGreetings.get_hello(language)
    "#{greeting}!"
  end
end
defmodule Demo.LocalisedGreetings do
  def get_hello("es"), do: {:ok, "Olla"}
  def get_hello("bg"), do: {:ok, "Здравей"}
  def get_hello(_), do: {:ok, "Hello"}
end

Нам нужно сделать две основные вещи:

  • напишите модульные тесты для модуля Demo.LocalisedGreetings.
  • Перепишите модульные тесты в модуле Demo.Greetings, чтобы использовать библиотеку Mox. Таким образом, мы можем заменить вызов Demo.LocalisedGreetings.get_greetings / 1 фиктивным значением и использовать его непосредственно в наших модульных тестах.
# localised_greetings_test.exs
defmodule Demo.LocalisedGreetingsTest do
  use ExUnit.Case, async: true
  alias Demo.LocalisedGreetings
  describe "get_hello/1" do
    test "return greeting if language is :es" do
      assert LocalisedGreetings.get_hello(:es) == {:ok, "Olla"}
    end
    test "return greeting if language is :bg" do
      assert LocalisedGreetings.get_hello(:bg) == {:ok, "Здравей"}
    end
    test "return greeting if language is not :es or :bg" do
      assert LocalisedGreetings.get_hello(:en) == {:ok, "Hello"}
    end
  end
end
# greetings_test.exs
defmodule Demo.GreetingsTest do
  use ExUnit.Case, async: true
  alias Demo.Greetings
  alias Demo.LocalisedGreetingsMock
  describe "say_hello/2" do
    test "return greeting when name is not a string" do
      lang = :en
      LocalisedGretingsMock
      |> expect(:get_hello, fn ^lang -> {:ok, Hello} end)
      assert Greetings.say_hello(nil, lang) == "Hello!"
    end
    test "return greeting when name is string" do
      name = "Velina"
      lang = :en
      LocalisedGretingsMock
      |> expect(:get_hello, fn ^lang -> {:ok, "Hello"} end)
      assert Greetings.say_hello(name, lang) == "Hello, Velina!"
    end
  end
end

Здесь мы сильно полагаемся на библиотеку Mox. Позвольте мне рассказать, что происходит в приведенных выше тестовых примерах:

  1. Мы даем значения нашим входным параметрам - имя и язык
  2. Мы добавляем фиктивное определение сортов. Если во время выполнения тестового примера кто-то вызовет LocalisedGreetings.get_hello/1 и передаст ему значение lang, мы пропустим get_hello/1 и просто вернем результат {:ok, “Hello”}
  3. Мы вызываем функцию Greetings.say_hello(name, lang) и проверяем ее результат.

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

say_hello/2 получит структуру %User{} вместо двух отдельных параметров. Поскольку теперь у нас есть новый сложный входной параметр, нам нужно будет переписать определение функции.

Боковое примечание: поскольку %User{} имеет значения по умолчанию для имени и языка, мы можем полагаться на то, что они всегда будут отличаться от nil.

# user.ex
defmodule Demo.User do
 defstruct name: “Jane”, language: :en
end
# greetings.ex
defmodule Demo.Greetings do
 alias Demo.LocalisedGreetings
 alias Demo.User

 def say_hello(%User{name: name, language: language}) do
   {:ok, greeting} = LocalisedGreetings.get_hello(language)
   “#{greeting}, #{name}!” 
 end

 def say_hello(_) do
   {:ok, greeting} = LocalisedGreetings.get_hello(:en)
   “#{greeting}!”
 end
end

defmodule Demo.LocalisedGreetings do
 def get_hello(“es”), do: {:ok, “Olla”}
 def get_hello(“bg”), do: {:ok, “Здравей”}
 def get_hello(_), do: {:ok, “Hello”}
end

В настоящее время код в нашем модуле Greetings относительно прост. У нас есть функция say_hello/1, которая принимает структуру в качестве входного параметра. Другими словами, для каждого написанного нами тестового примера нам нужно заранее инициализировать значение пользователя.

Так что, если бы вместо этого в нашем Greetings модуле была сотня функций. В этом случае нам потребуется выполнить инициализацию как минимум сто раз. В результате наши тестовые примеры будут загромождены повторяющимся кодом.

К счастью, у этой проблемы есть изящное решение. Мы можем объединить мощь ExUnit и Ex_Machina, чтобы решить проблему создания данных для наших тестов.

Мы можем использовать библиотеку Ex_Machina для создания новой фабрики, посвященной структуре% User {}.

defmodule Demo.Factory do
  use ExMachina

  def user_factory do
    %Demo.User{name: "Jane", language: :en}
  end
end

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

build(:user, %{name: Velina, lang: :en})

Это выражение выше оценивается как %User{name: “Velina”, language: :en}

Наконец, мы можем использовать функцию ExUnit.Callbacks.setup/1, чтобы гарантировать, что мы создадим экземпляр пользовательской структуры и передадим его в уважаемые тестовые примеры.

Полная реализация кода будет выглядеть так:

# localised_greetings_test.exs

defmodule Demo.LocalisedGreetingsTest do
  use ExUnit.Case, async: true
  alias Demo.LocalisedGreetings

  describe "get_hello/1" do
    test "return greeting if language is :es" do
      assert LocalisedGreetings.get_hello(:es) == {:ok, "Olla"}
    end

    test "return greeting if language is :bg" do
      assert LocalisedGreetings.get_hello(:bg) == {:ok, "Здравей"}
    end

    test "return greeting if language is not :es or :bg" do
      assert LocalisedGreetings.get_hello(:en) == {:ok, "Hello"}
    end
  end
end

# factory.ex
defmodule Demo.Factory do
  use ExMachina

  def user_factory do
    %Demo.User{name: "Jane", language: :en}
  end
end

# greetings_test.exs
defmodule Demo.GreetingsTest do
  use ExUnit.Case, async: true

  alias Demo.User  
  alias Demo.Greetings
  alias Demo.LocalisedGreetingsMock
  import Demo.Factory

  describe "say_hello/1" do
    setup [:setup_user]

    test "return default greeting" do
      LocalisedGretingsMock
      |> expect(:get_hello, fn _ -> {:ok, Hello} end)

      assert Greetings.say_hello(nil) == "Hello!"
    end

    test "return greeting for user", %{user: user} do
      language = user.language
      LocalisedGretingsMock
      |> expect(:get_hello, fn ^language -> {:ok, Hello} end)

      assert Greetings.say_hello(user) == "Hello, Velina!"
    end
  end

  defp setup_user(_context) do
    user = build(:user, %{name: Velina, lang: :en})
    %{user: user}
  end
end

На этом я завершаю введение в написание модульных тестов на Эликсире. Я оптимистично настроен, что вы можете уйти от этой статьи с некоторой ясностью в отношении того, что вам нужно знать, чтобы начать писать тесты в Elixir.