От переводчика: «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 и получайте каждую неделю интересные статьи на русском.
Другие части:
- Вступление
- Авторизация
- Добавляем роли
- Добавляем обработку ролей в контроллерах
- Подключаем ExMachina
- Скоро...
Успехов в изучении, оставайтесь с нами!
Автор: jarosluv