От переводчика: «Elixir и Phoenix — прекрасный пример того, куда движется современная веб-разработка. Уже сейчас эти инструменты предоставляют качественный доступ к технологиям реального времени для веб-приложений. Сайты с повышенной интерактивностью, многопользовательские браузерные игры, микросервисы — те направления, в которых данные технологии сослужат хорошую службу. Далее представлен перевод серии из 11 статей, подробно описывающих аспекты разработки на фреймворке Феникс казалось бы такой тривиальной вещи, как блоговый движок. Но не спешите кукситься, будет действительно интересно, особенно если статьи побудят вас обратить внимание на Эликсир либо стать его последователями.

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


На данный момент наше приложение основано на:

  • Elixir: v1.3.1
  • Phoenix: v1.2.0
  • Ecto: v2.0.2
  • Comeonin: v2.5.2

Введение


Как вы заметили, в процессе написания этого движка мы используем всего несколько библиотек. Сейчас добавим ещё одну под названием ExMachina. Она является аналогом Factory Girl из Ruby.

Что же это?


Как только что упоминалось, ExMachina спроектирована по образу Factory Girl - реализации паттерна Фабрика из Ruby (также от замечательных ребят из Thoughtbot). Мы исходим из того, что было бы здорово добавлять в тесты различные модели со связями, не переписывая из раза в раз код для их создания. Можно добиться того же самостоятельно с помощью вспомогательных модулей, включающих простые функции для генерации моделей. Но тогда всё сведётся к постоянному созданию подобных модулей для каждого необходимого набора данных, для каждой связи и так далее. Это непременно успеет надоесть.

Приступаем


Начнём с открытия файла mix.exs для добавления ExMachina к спискам deps и application. Для этого просто вставим в список зависимостей ещё одну запись для ExMachina сразу после ComeOnIn:

defp deps do
  [{:phoenix, "~> 1.2.0"},
   {:phoenix_pubsub, "~> 1.0"},
   {:phoenix_ecto, "~> 3.0"},
   {:postgrex, ">= 0.0.0"},
   {:phoenix_html, "~> 2.6"},
   {:phoenix_live_reload, "~> 1.0", only: :dev},
   {:gettext, "~> 0.11"},
   {:cowboy, "~> 1.0"},
   {:comeonin, "~> 2.5.2"},
   {:ex_machina, "~> 1.0"}]
end

А затем добавим :ex_machina в список используемых приложений:

def application do
  [mod: {Pxblog, []},
   applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
                  :phoenix_ecto, :postgrex, :comeonin, :ex_machina]]
end

Запустите следующую команду, чтобы убедиться в готовности и правильной настройке компонентов приложения:

$ mix do deps.get, compile

Если всё пройдёт хорошо, то вы должны увидеть на выходе сообщение об установке ExMachina и успешной компиляции проекта! Перед тем как мы станем изменять код, вам нужно запустить mix test и убедиться, ради дополнительной надёжности, что все тесты зелёные.

Добавляем первую фабрику для ролей


Нам нужно создать фабричный модуль и сделать его доступным для всех тестов. Я предпочитаю делать это без раздувания тестов. Для этого просто кинем файл модуля с фабриками в директорию test/support и затем пропишем его импорт в необходимых нам тестах.

Итак, давайте начнём с создания файла test/support/factory.ex:

defmodule Pxblog.Factory do
  use ExMachina.Ecto, repo: Pxblog.Repo

  alias Pxblog.Role
  alias Pxblog.User
  alias Pxblog.Post

  def role_factory do
    %Role{
      name: sequence(:name, &"Test Role #{&1}"),
      admin: false
    }
  end
end

Мы назвали его Factory, потому что такое имя отражает всю суть этого модуля. Затем мы будем использовать специальные фабричные функции. Они сопоставляют с образцом подаваемый на вход атом, который определяет какой тип фабрики собирать/создавать. Так как эта библиотека довольно близка к Factory Girl, она так же приносит с собой некоторые соглашения по именованию, которые важно знать. Первым таким названием будет build. Функция build означает, что модель (не ревизия) будет собрана без сохранения в базу данных. Вторым соглашением станет названии функции insert, которая наоборот сохраняет модель в базе данных, тем самым создавая её.

Нам также нужно указать use ExMachina.Ecto, чтобы ExMachina стала использовать Ecto в качестве слоя Repo и вела себя соответствующе при создании моделей, ассоциаций и т.п. Нам также нужно добавить псевдонимы всем моделям, для которых мы будем писать фабрики.

Функция role_factory должна просто возвращать структуру Role, которая определяет свойства по умолчанию. Эта функция поддерживает только арность 1.

Кусочек с функцией sequence довольно любопытен. Нам нужно сгенерировать уникальное название для каждой роли. Поэтому сделаем его последовательно генерируемым. Для этого мы берём функцию sequence, в которую передаём два аргумента: первым — название поля, для которого хотим генерировать последовательность, вторым — анонимную функцию, которая возвращает строку и интерполирует значение внутри неё. Давайте взглянем на эту функцию:

&”Test Role #{&1}”

Если вы неплохо знакомы с Elixir, то, возможно, узнали альтернативный способ записи анонимных функций. Это приблизительно переводится как:

fn x ->
  "Test Role #{x}"
end

Так что объяснить функцию sequence можно таким образом:

sequence(:name, fn x ->
  "Test Role #{x}"
end)

Наконец, установим флаг администратора в положение false, т.к. мы используем это значение в качестве условия по умолчанию. Администраторскую роль мы сможем создать указав это явно. Другие более сложные возможности ExMachina давайте обсудим немного позже. Теперь потратим некоторое время на объединение нашей новой фабрики Role c тестами контроллеров.

Добавляем фабрику Role в тесты контроллеров


Сначала откройте файл test/controllers/user_controller_test.exs. Наверху, в блоке setup добавим использование нашей новой функции TestHelper.create_role:

# ...
import Pxblog.Factory

@valid_create_attrs %{email: "test@test.com", username: "test", password: "test", password_confirmation: "test"}
@valid_attrs %{email: "test@test.com", username: "test"}
@invalid_attrs %{}

setup do
  user_role = insert(:role)
  {:ok, nonadmin_user} = TestHelper.create_user(user_role, %{email: "nonadmin@test.com", username: "nonadmin", password: "test", password_confirmation: "test"})

  admin_role = insert(:role, admin: true)
  {:ok, admin_user}    = TestHelper.create_user(admin_role, %{email: "admin@test.com", username: "admin", password: "test", password_confirmation: "test"})

  {:ok, conn: build_conn(), admin_role: admin_role, user_role: user_role, nonadmin_user: nonadmin_user, admin_user: admin_user}
end
# ...

Но перед этим импортируем сам фабричный модуль. В строке 10 мы просто добавляем роль, используя фабрику :role. В строке 13 мы поступаем аналогичным образом, но переопределяем флаг администратора в значение true.

Сохраните файл и перезапустите тесты. Все они должны по-прежнему проходить! Теперь давайте напишем фабрику для пользователей, которая также создаёт и связи.

Добавляем фабрику для пользователей


Взгляните на фабрику для пользователей.

def user_factory do
  %User{
    username: sequence(:username, &"User #{&1}"),
    email: "test@test.com",
    password: "test1234",
    password_confirmation: "test1234",
    password_digest: Comeonin.Bcrypt.hashpwsalt("test1234"),
    role: build(:role)
  }
end

В основном, эта фабрика совпадает с тем, что мы написали ранее для создания ролей. Но есть пара подводных камней, с которыми нам предстоит иметь дело. Выше, на строке 7, вы можете увидеть, что мы устанавливаем значение password_digest равным значению хэша пароля password (так как мы имитируем вход пользователя, нам нужно добавить и это). Мы просто вызываем модуль Bcrypt из Comeonin и используем функцию hashpwsalt, передавая в неё то же самое значение, что и в поля password/password_confirmation. На следующей строке мы также устанавливаем role в качестве ассоциации. Мы используем функцию build и передаём в неё название ассоциации, которую хотим собрать, в виде атома.

Модифицировав фабрику пользователей, давайте вернёмся к файлу test/controllers/user_controller_test.exs.

setup do
  user_role     = insert(:role)
  nonadmin_user = insert(:user, role: user_role)

  admin_role = insert(:role, admin: true)
  admin_user = insert(:user, role: admin_role)

  {:ok, conn: build_conn(), admin_role: admin_role, user_role: user_role, nonadmin_user: nonadmin_user, admin_user: admin_user}
end

Теперь мы окончательно заменим все вызовы к TestHelper вызовами к фабрике. Мы берём роль и передаём её в фабрику, чтобы создать пользователя с правильной ролью. Затем сделаем то же самое с администратором, но при этом нам не нужно изменять наши тесты!

Запустите их и убедитесь, что они по-прежнему зелёные. Можем продолжать.

Добавляем фабрику для постов


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

Здесь нет ничего нового, так что давайте просто изменим файл test/controllers/post_controller_test.exs:

def post_factory do
  %Post{
    title: "Some Post",
    body: "And the body of some post",
    user: build(:user)
  }
end

Ещё раз, мы выполняем import модуля Pxblog.Factory, чтобы наши тесты знали где находится фабрика, к которой мы направляем вызовы. Затем мы заменяем все шаги по созданию поста в блоке setup вызовом фабрики. С помощью функции insert создаётся структура role, которая затем используется для создания пользователя через фабрику, который, наконец, используется для создания связанного с ним поста… Всего-то!

Запустите тесты. Они снова стали зелёными!

С этого места, всё остальное лишь дополнительная работа. Вернёмся назад и заменим все вызовы к TestHelper вызовами Factory. В этом нет особо ничего нового или захватывающего, так что я не буду уделять излишнее внимание объяснение деталей.

Другие способы подключения фабрик


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

Добавьте псевдоним в блок using в файле test/support/model_case.ex:

using do
  quote do
    alias Pxblog.Repo
    import Ecto
    import Ecto.Changeset
    import Ecto.Query
    import Pxblog.ModelCase
    import Pxblog.Factory
  end
end

И файл test/support/conn_case.ex:

using do
  quote do
    # Import conveniences for testing with connections
    use Phoenix.ConnTest

    alias Pxblog.Repo
    import Ecto
    import Ecto.Changeset
    import Ecto.Query

    import Pxblog.Router.Helpers
    import Pxblog.Factory

    # The default endpoint for testing
    @endpoint Pxblog.Endpoint
  end
end

Другие возможности ExMachina


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

build_pair(:factory, attrs)    <- Builds 2 models
build_list(n, :factory, attrs) <- Builds N models

Вы также можете сохранить модель, которую вы собрали с помощью метода build вызовом create на ней:

build(:role) |> insert

Другие ресурсы


Для дополнительной информации по использованию ExMachina зайдите на Github страницу. Вы также можете посетить технический блог Thoughbot, где создатели разместили прекрасный анонс ExMachina и некоторые другие способы её использования.

Подведём итоги


Сначала, надо сказать, я был немного насторожен, вспоминая как реализовывал ранее некоторые вещи с помощью Factory Girl. Я боялся, что здесь всё пойдёт так же. Но Elixir защищает нас от самих себя, что помогает найти баланс при тестировании. Синтаксис чёткий и чистый. Количество необходимого кода уменьшилось значительно. Огромное спасибо славным ребятам из Thoughtbot за ещё одну чрезвычайно полезную библиотеку.

Заключение от Вуншей


Сегодня очень короткое заключение — просто подписывайтесь на наше сообщество Elixir и получайте каждую неделю интересные статьи на русском.

Другие части:

  1. Вступление
  2. Авторизация
  3. Добавляем роли
  4. Добавляем обработку ролей в контроллерах
  5. Подключаем ExMachina
  6. Скоро...

Успехов в изучении, оставайтесь с нами!
Поделиться с друзьями
-->

Комментарии (11)


  1. Fedcomp
    07.12.2016 10:54
    -1

    Все еще не вижу php хаба, он сюда больше подойдет.


    1. jarosluv
      07.12.2016 12:18

      Почему вы так думаете?


      1. Fedcomp
        07.12.2016 17:37

        А почему вы добавляете хабы к которым ваша статья не относится?


        1. jarosluv
          07.12.2016 19:30

          В этой статье проводится параллель с Factory Gril из Ruby. Ещё вопросы?


          1. Fedcomp
            13.12.2016 08:56
            -1

            И что? если вы проводите где то паралель с другим языком (одной-двумя отсылками) это теперь повод пихать это в лишние хабы? почему бы сюда javascript не засунуть? тоже язык программирования.


      1. jukkagrao
        07.12.2016 19:30

        Я думаю это было что-то вроде сарказма по поводу Ruby On Rails никаким боком сюда не относящегося


        1. jarosluv
          07.12.2016 19:35

          Я просто не могу понять мотивацию человека из раза в раз писать одно и то же в комментариях, хотя я всегда отвечаю почему добавляю Ruby On Rails в список хабов. Сейчас, с появлением хаба под Elixir, будем ждать его наполнения людьми.

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

          И третья причина. Я буду добавлять этот хаб во все статьи из серии, чтобы никто из текущих читателей не пропустил новую часть.


          1. imgen
            07.12.2016 21:01

            В чём-то он прав. Разработчикам php это вполне может быть интересно.


          1. Source
            09.12.2016 13:54

            Также повторюсь, что сейчас проводится чёткая параллель между Elixir и Ruby

            Это очень хреновая параллель, т.к. языки не имеют ничего общего, кроме отдалённо похожего синтаксиса и частично совпадающих названий функций в stdlib. По факту Elixir имеет в сотни раз больше общего с Erlang и с Lisp, чем с Ruby.
            Хуже параллель проводить только между Phoenix и Rails, там вообще принципиально разная идеология в архитектуру заложена.


            1. jarosluv
              09.12.2016 14:42
              +1

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


              1. Source
                09.12.2016 17:34

                Если понимаете, то должны также понимать что все эти параллели — это медвежья услуга новичкам, ну и соответственно вред всему Elixir-сообществу.
                Во-первых, пытаться тащить привычки из Ruby в Elixir — это самый сложный путь.
                Тем, кто хочет изучить Elixir, эффективнее всего будет отложить все знания, связанные с Ruby, на отдельную полочку и не вспоминать про них пока программируешь на Elixir.
                Во-вторых, библиотеки по кальке скопированные с Ruby только привносят проблемы с пониманием того, как работает Elixir и какова его область применения. Этому надо противостоять по мере сил и поддерживать идиоматичные для Elixir библиотеки.