先日のLT会でちらっと話しましたが、Elixirのテストデータ生成ツールにExMachinaというのがあります。

たとえば、次のようなブログ記事のschemaがあるとして、

defmodule Blog.Article do
  use Ecto.Schema

  schema "articles" do
    field :title, :string
    field :body, :string
    field :is_draft, :boolean

    belongs_to :author, Blog.User
    timestamps(type: :utc_datetime)
  end

end

対応するFactoryを次のように書いておきます。

defmodule Blog.Factory do

  use ExMachina.Ecto, repo: Blog.Repo

  def article_factory do
    # sequenceを使うことで自動で連番を付与してくれる
    # unique制約がついているカラムのテストデータを作るのに便利
    title = sequence(:title, &"Use ExMachina! (Part #{&1})")

    %Blog.Article{
      title: title,
      body: "test body",
      is_draft: true,
      author: build(:user),
    }
  end

end

このFactory関数を利用して、テストコード内でDBへのinsertや登録用の文字列のMapを取得が楽にできます。

test "some test" do
  # `*_factory`の`*`の部分をatomで渡せばFactory関数を呼べる
  # build(:user)の部分はuserのレコードを追加してうまいことuser_idを採番してくれる(別途user_factoryが必要)
  article = Blog.Factory.insert(:article)

  # 第2引数で特定のフィールドを変更することもできる
  draft_article = Blog.Factory.insert(:article, %{is_draft: false})

  # %{"title" => "test title", ...} のような文字列のMapを取得。controllerのテストなどに使える。
  params = string_params_for(:article)

  ...
end

リレーションをまとめてinsertしてくれるなど、上手に使えばテストコードの生成が非常に楽になるツールです。

ただ、

  • 自前でFactory関数を生やして、、、
  • Schemaのfieldをそれぞれ書き起こして、、、

というのが若干手間です。

schema→factoryへの置き換えを行うシェル芸を書いたりしていたのですが、せっかくなので勉強ついでに雛形生成の処理をMixタスク化して公開してみました。

ExMachinaGen

何ができるのか

mix ex_machina.gex <schema module> というMixタスクを実行すると、引数で渡したschemaモジュールに基づいてfactory関数をそれっぽく生成します。 titleフィールドがstringであれば、title: "test title"、is_draftフィールドがbooleanであればis_draft: trueのように、型に合った初期値を固定値で埋めて生成します。

先の例だと、

$ mix ex_machina.gen Blog.Article

を実行すると以下のようなファイルが吐き出されます。

# test/support/factory/article_factory.ex
defmodule Blog.ArticleFactory do
  defmacro __using__(_opts) do
    quote do
      def article_factory do
        %Blog.Article{
          author: build(:user),
          body: "test body",
          draft: true,
          inserted_at: ~U[2019-01-01 00:00:00Z],
          title: "test title",
          updated_at: ~U[2019-01-01 00:00:00Z]
        }
      end
    end
  end
end

https://github.com/thoughtbot/ex_machina/tree/master#splitting-factories-into-separate-files に書いてあるやり方に則り、マクロを利用してファイルを分割しています。

とりあえずエイっとファイルを吐き出して、細かいチューニングはファイルをいじっていこうというコンセプトです。

気づき

生成系のコマンドは

  • mix phx.gen.context
  • mix ecto.gen.migration

など普段よく叩いていると思うので、それらのコードを参考にするのが良かったです。

ハマりどころとしては操作したいschemaのモジュールがmodule XXX is not availableが出て見つからない状況になり、そこそこハマりました。

depsに追加して使うことが想定される場合、コマンドを叩く側のアプリケーションと別アプリケーションになるので、Mix.Task.run("loadpaths")を実行しておかないとMixタスクを叩いた側のアプリケーションのモジュールを見つけてくれないようです。

参考: https://elixirforum.com/t/load-application-and-modules-from-another-mix-project/22681

中の詳細な挙動はまだ追えてませんが、ectoの実装にも使われているし、現状これで解決したのでまぁ良しとしています。

https://github.com/elixir-ecto/ecto/blob/master/lib/mix/ecto.ex#L63

着想からhexへの公開までtotalで5h程度でしょうか。これぐらいの規模感であれば慣れてくれば便利タスクをサクッと作っていけそうです。

今後

Fakerを入れてよりダミーデータの生成も楽にしていきたいとかぼんやりと思っています。

触ったことないですが、https://github.com/elixirs/faker と組み合わせることができたら良さそうな気がしています。

まとめ

エイっと作ったのでバグってたらこっそり教える or 堂々とPRください!!w

ExMachinaGen