Open3

Elixir PhoenixでTrait(FactoryBot Trait)ライクを使うための習作

tatotato

利用イメージ

  • user_fixture([:japanese])
  • user_fixture(email: "test@example.com")
  • user_fixture([:japanese, email: "test@example.com"])
  • user_fixture([:japanese, :male, email: "test@example.com"])

追記:下記を検討のこと
https://github.com/thoughtbot/ex_machina


たたき台バージョン

共通定義

defmodule DataTrait do
  def trait do
    quote do
      defp trait(obj, []), do: obj

      defp trait(obj, [head | tail]) when is_atom(head) do
        _trait(obj, head) |> trait(tail)
      end

      defp trait(obj, list) do
        obj
        |> Map.merge(Map.new(list))
      end

      defp _trait(obj, :touch), do: obj
    end
  end

  defmacro __using__([]) do
    apply(__MODULE__, :trait, [])
  end
end

fixtureモジュール側

defmodule UsersFixtures do
  use DataTrait

  def user_fixture(traits \\ []) do
    %User{}
    |> trait(traits)
    |> Repo.insert!()
  end

  defp _trait(user, :japanese),
    do: Map.put(user, :language, "japanese")

  defp _trait(user, :man),
    do: Map.put(user, :sex, "male")
end
tatotato
  • 実用に耐えそう...?
  • 複雑なパターンはおとなしくuser_hoge_fixture()で分ける
tatotato

使ってみて変更してみたバージョン

  • 命名をやや変更
  • user_fixture(:japanese) にも対応(1要素ならリストにしなくてよい)
  • 名前が各Fixturesで被るのでprefixを指定できるように対応。 use DataTraits, "user"のように指定する
    • 結果、DataTraitsの可読性がぐっと下がったけれど仕方なしか
  • def <namespace>_trait(obj, :defaults) を個別実装することを共通処理での前提に変更
    • 例えば、def user_trait(obj, :defaults) のような定義をuseするモジュールで必要とする
  • キーがStringなMapとAtomなMapとが欲しいときがあるので、用意してしまった
/test/support/data_traits.ex
defmodule DataTraits do
  def traits(name_space) do
    prefix = name_space <> "_"
    func_name_traits = String.to_atom(prefix <> "traits")
    func_name_trait = String.to_atom(prefix <> "trait")

    quote do
      defp unquote(func_name_traits)(trait) when is_atom(trait) do
        unquote(func_name_traits)(%{}, [:defaults, trait])
      end

      defp unquote(func_name_traits)(traits) when is_list(traits) do
        unquote(func_name_traits)(%{}, [:defaults] ++ traits)
      end

      defp unquote(func_name_traits)(obj, []), do: obj

      defp unquote(func_name_traits)(obj, [head | tail]) when is_atom(head) do
        unquote(func_name_trait)(obj, head) |> unquote(func_name_traits)(tail)
      end

      defp unquote(func_name_traits)(obj, [{key, value} | tail]) do
        obj
        |> Map.put(key, value)
        |> unquote(func_name_traits)(tail)
      end

      defp unquote(func_name_traits)(obj, [head | tail]) when is_list(head) do
        obj
        |> Map.merge(Map.new(head))
        |> unquote(func_name_traits)(tail)
      end

      defp unquote(func_name_trait)(obj, :touch), do: obj

      defp unquote(func_name_trait)(obj, :to_params) do
        obj
        |> Map.new(fn
          {key, value} when is_atom(key) -> {Atom.to_string(key), value}
          other -> other
        end)
      end

      defp unquote(func_name_trait)(obj, :to_attrs) do
        obj
        |> Map.new(fn
          {key, value} when is_bitstring(key) -> {String.to_atom(key), value}
          other -> other
        end)
      end
    end
  end

  defmacro __using__(name_space) do
    apply(__MODULE__, :traits, [name_space])
  end
end

fixtureモジュール側の例

defmodule MyApp.ThemesFixtures do
  @moduledoc """
  This module defines test helpers for creating
  entities via the `MyApp.Themes` context.
  """

  use DataTraits, "theme"

  @doc """
  Generate a theme.
  """
  def theme_fixture(traits \\ []) do
    params = theme_params(traits)
    {:ok, theme} = Themes.create_theme(params)

    theme
  end

  def theme_params(traits) do
    theme_traits(List.wrap(traits) ++ [:to_params])
  end

  defp theme_trait(obj, :defaults) do
    obj
    |> Map.merge(%{
      name: "テーマ",
    })
  end

  defp theme_trait(obj, :invalid), do: Map.put(obj, :name, "")