Pull to refresh

Создание движка для блога с помощью Phoenix и Elixir / Часть 5. Подключаем ExMachina

Reading time 7 min
Views 3.9K
Original author: Brandon Richey


От переводчика: «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. Поддержка Markdown
  7. Добавляем комментарии
  8. Заканчиваем с комментариями
  9. Каналы
  10. Тестирование каналов
  11. Заключение


Успехов в изучении, оставайтесь с нами!
Tags:
Hubs:
+8
Comments 11
Comments Comments 11

Articles