phx.gen.authのテストをfixtureからExMachinaに置き換えてみた

# はじめに
本記事はphx.gen.authで自動生成されるテストで、fixutesではなくExMachinaを使用するように置き換えた記事です。

# ExMachinaとは
Railsのテストデータ作成ライブラリFactoryBotを作成しているthoughtbot社が提供しているライブラリで、
FactoryBotと似たような使用感でテストデータの作成(build, insert)、パラメーターのみ生成(params_for)等が行えます


# 下準備
では実際に作っていきましょう

## プロジェクト作成

```
mix phx.new blog
cd blog
mix ecto.create
```

## phx.gen.auth
```
mix phx.gen.auth Accounts User users
mix deps.get
mix ecto.migrate
```

## ExMachinaの追加
以下を追加して mix deps.getを行いましょう

```mix.exs
defmodule Blog.MixProject do
  ...
  defp deps do
    [
      ...
      {:ex_machina, "~> 2.7.0", only: :test},
    ]
  end
end
```

# setup
Readmeにあるように test_helperのExUnit.start()の前にex_machinaをスタートするコードを追加

```test/test_helper.exs
{:ok, _} = Application.ensure_all_started(:ex_machina) # 追加
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Blog.Repo, :manual)
```

readmeでは１つのファイルにまとめてfactory methodを記述していますが、どうせ増えてきて分割するので分けていきます

```test/support/factory.ex
defmodule Blog.Factory do
  use ExMachina.Ecto, repo: Blog.Repo
  use Blog.Factories.Users
end
```
readme通りに書いていきます
insert(:user)で実行できるようにdefmacro内の更にquote do配下で記述するくらいに認識しておいてください
insertしてもhashed_passwordは作成してくれなかったので、Bcrypt.hash_pwd_saltで暗号化した文字列を保存するようにします

※ 2022/04/18 追記
特定のパラメータのみを生成したい場合は unique_user_email_factoryみたいに
関数名 生成したい値 + _factory 引数にattrを入れて関数を作成しましょう
使うときは `build(:unique_user_email)`とします
参考: https://github.com/thoughtbot/ex_machina#non-map-factories

```test/support/factories/users.ex
defmodule Blog.Factories.Users do
  defmacro __using__(_opts) do
    alias Blog.Accounts.User
    quote do
      def user_factory do
        %User{
          email: sequence(:email, &"user#{&1}@example.com"),
          password: "hello world!",
          hashed_password: Bcrypt.hash_pwd_salt("hello world!")
        }
      end

      # 2022/04/18 追記
      def unique_user_email_factory(_attr) do
        sequence(:unique_user_email, &"user#{&1}@example.com")
      end 
    end
  end
end
```



ConnCaseを使うテストでFactoryが使えるようにimportします
register_and_log_in_userでfixtureを使っていたので insert(:user)でユーザーを作成するようにします
```test/support/conn_case.ex
defmodule BlogWeb.ConnCase do
  ...
  use ExUnit.CaseTemplate

  using do
    quote do
      # Import conveniences for testing with connections
      import Plug.Conn
      import Phoenix.ConnTest
      import BlogWeb.ConnCase
      import Blog.Factory # 追加

      alias BlogWeb.Router.Helpers, as: Routes

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

  ...
  def register_and_log_in_user(%{conn: conn}) do
    user = Blog.Factory.insert(:user) # fixtureからinsertに変更
    %{conn: log_in_user(conn, user), user: user}
  end
end
```
DataCaseを使用するケースでも使えるようにimportします

```test/support/data_case.ex
defmodule Blog.DataCase do
  ...
  use ExUnit.CaseTemplate

  using do
    quote do
      alias Blog.Repo

      import Ecto
      import Ecto.Changeset
      import Ecto.Query
      import Blog.DataCase
      import Blog.Factory # 追加
    end
  end
  ...
end
```
これで準備が整ったので置き換えていきましょう

# 置き換え
最初に`accounts_fixtures`の`unique_user_email`と`extract_user_token`をコピーして`accounts_fixtures.ex`を削除します

```test/support/factory.ex
defmodule Blog.Factory do
  use ExMachina.Ecto, repo: Blog.Repo
  use Blog.Factories.Users

  def unique_user_email, do: "user#{System.unique_integer()}@example.com"

  def extract_user_token(fun) do
    {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
    [_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
    token
  end
end
```
# 置換
以下の左側のコードを左側に置き換えます
vscodeならcmd + shift + f押したあと、左端にtoggleボタンがあるのでそれをクリックすると置換モードになりますのでそこに置換する文字を入れてください

```
fixture_user() -> insert(:user)
valid_user_password() -> user.password
import Blog.AccountsFixtures -> 削除
```

## passwordの明示的な削除
上記の置換が終わってテストを実行すると factoryのuserはpasswordを保持したままになるので、`is_nil(user.password)`となっている箇所がコケます

これを回避するために該当メソッドで明示的にpassowrdをnilにするようにしますが、あまりお行儀は良くないのでスマートな方法があったら教えて下さい m(_ _)m
```lib/blog/accounts.ex
defmodule Blog.Accounts do
  ...
  def update_user_password(user, password, attrs) do
    changeset =
      user
      |> User.password_changeset(attrs)
      |> User.validate_current_password(password)

    Ecto.Multi.new()
    |> Ecto.Multi.update(:user, changeset)
    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
    |> Repo.transaction()
    |> case do
      {:ok, %{user: user}} -> {:ok, Map.put(user,:password, nil)} # passwordをnilで上書き
      {:error, :user, changeset, _} -> {:error, changeset}
    end
  end
  ...
  def reset_user_password(user, attrs) do
    Ecto.Multi.new()
    |> Ecto.Multi.update(:user, User.password_changeset(user, attrs))
    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
    |> Repo.transaction()
    |> case do
      {:ok, %{user: user}} -> {:ok, Map.put(user, :password, nil)} # passwordをnilで上書き
      {:error, :user, changeset, _} -> {:error, changeset}
    end
  end
end
```
# params_for
User構造体ではなくフォームパラメータのMapが欲しい場合はparams_for()を使うこと解決します

```test/blog_web/controllers/user_registration_controller_test.exs
defmodule BlogWeb.UserRegistrationControllerTest do
  use BlogWeb.ConnCase, async: true

  describe "POST /users/register" do
    @tag :capture_log
    test "creates account and logs the user in", %{conn: conn} do
      %{email: email} = user = params_for(:user) # params_forに置き換え

      conn =
        post(conn, Routes.user_registration_path(conn, :create), %{
          "user" => user
        })

      assert get_session(conn, :user_token)
      assert redirected_to(conn) == "/"

      # Now do a logged in request and assert on the menu
      conn = get(conn, "/")
      response = html_response(conn, 200)
      assert response =~ email
      assert response =~ "Settings</a>"
      assert response =~ "Log out</a>"
    end
  end
```

```test/blog/accounts_test.exs
defmodule Blog.AccountsTest do
  describe "register_user/1" do
    ...
    test "registers users with a hashed password" do
      %{email: email} = user = params_for(:user) # params_forに置き換え
      {:ok, user} = Accounts.register_user(user)
      assert user.email == email
      assert is_binary(user.hashed_password)
      assert is_nil(user.confirmed_at)
      assert is_nil(user.password)
    end
  end
  ...

  describe "change_user_registration/2" do
    ...
    test "allows fields to be set" do
      user = params_for(:user) # params_forに置き換え
      changeset = Accounts.change_user_registration(%User{}, user)

      assert changeset.valid?
      assert get_change(changeset, :email) == user.email
      assert get_change(changeset, :password) == user.password
      assert is_nil(get_change(changeset, :hashed_password))
    end
    ...
  end
```

# 最後に
以上でphx.gen.authでExMachinaを使うように置き換えてテストがパスするようになりました
次はリレーション周りなどもできればと思います
ありがとうございました

# コード
https://github.com/thehaigo/phx_gen_auth_test_replace_ex_machina

# 差分
https://github.com/thehaigo/phx_gen_auth_test_replace_ex_machina/commit/b5cb6b18cdff37bcabb3e2d6118f62aaf4f60599

# 参考ページ
https://github.com/thoughtbot/ex_machina
https://github.com/thoughtbot/ex_machina#non-map-factories