Moxを使ってみる

# Brightの対象
Webアプリ開発 Elixir > クラス1 > Elixirテスト／デバッグ > ユニットテスト > mox

# 動作環境

```
$ cat /etc/os-release | grep VER
VERSION="20.04.4 LTS (Focal Fossa)"
VERSION_ID="20.04"
VERSION_CODENAME=focal

$ elixir -v
Erlang/OTP 24 [erts-12.3.1] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit]

Elixir 1.13.0 (compiled with Erlang/OTP 24)
```

# プロジェクトの作成

```
$ mix new ymn
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/ymn.ex
* creating test
* creating test/test_helper.exs
* creating test/ymn_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd ymn
    mix test

Run "mix help" for more commands.
```

# 動作確認
```
$ cd ymn
$ mix test
Compiling 1 file (.ex)
Generated ymn app
..

Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 doctest, 1 test, 0 failures

Randomized with seed 33659
```

# ビヘイビアを定義する

```lib/ymn_behaviour.ex

defmodule YmnBehaviour do
  @callback say(msg :: String.t()) :: :ok
end

```

`lib/ymn.ex`を修正
```diff_elixir  

defmodule Ymn do
+  @behaviour YmnBehaviour
  @moduledoc """
  Documentation for `Ymn`.
  """

  @doc """
  Hello world.

  ## Examples

      iex> Ymn.hello()
      :world

  """
  def hello do
    :world
  end
end
```

試しにsayを未定義のままtestを実行
sayの関数の警告が出ることを確認

```
mix test
Compiling 1 file (.ex)
warning: function say/1 required by behaviour YmnBehaviour is not implemented (in module Ymn)
  lib/ymn.ex:1: Ymn (module)

..

Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 doctest, 1 test, 0 failures
```
# sayを実装

`lib/ymn.ex`を修正
```diff_elixir  
defmodule Ymn do
  @behaviour YmnBehaviour

  @moduledoc """
  Documentation for `Ymn`.
  """

  @doc """
  Hello world.

  ## Examples

      iex> Ymn.hello()
      :world

  """
  def hello do
    :world
  end

+ def say(msg) do
+   IO.puts("Hello!")
+   IO.puts(msg)
+   :ok
+ end

end

```

# 動作確認

```elixir
iex -S mix
Erlang/OTP 24 [erts-12.3.1] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit]

Interactive Elixir (1.13.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Ymn.say("hoge")
Hello!
hoge
:ok
```

# mixにMoxの定義する

`mix.exs`を修正する
```diff_elixir
defmodule Ymn.MixProject do
  use Mix.Project

  def project do
    [
      app: :ymn,
      version: "0.1.0",
      elixir: "~> 1.13",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
+     {:mox, "~> 1.0"}
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end
end
```

mix deps.getをしておく

```
$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
New:
  mox 1.0.1
* Getting mox (Hex package)
```

# テスト環境と実行環境で切り替える仕組みを作る


`lib/ymn_behaviour.ex`を修正する

```diff_elixir
defmodule YmnBehaviour do
  @callback say(msg :: String.t()) :: :ok

+ def get_ymn_module do
+   Application.get_env(:ymn, :ymn_module, Ymn)
+ end
end
```

# テスト環境で動作するように修正
この段階ではまだMoxを使わずに実装してみる
※Doctestも実験の為廃止してます


`test/ymn_test.exs`を修正

```diff_elixir
defmodule YmnTest do
  use ExUnit.Case
- doctest Ymn

- test "greets the world" do
-   assert Ymn.hello() == :world
- end

+ test "say" do
+   ymn_module = YmnBehaviour.get_ymn_module()
+   assert ymn_module.say("hoge") == :ok
+ end

end
```

現時点で動作確認をしてみる

```
$ mix test
Hello!
hoge
.

Finished in 0.00 seconds (0.00s async, 0.00s sync)
1 test, 0 failures
```

# ここから本題Moxを定義してみる

`test/test_helper.exs`を修正


```diff_elixir
  ExUnit.start()

+ Mox.defmock(YmnMock, for: YmnBehaviour)
+ Application.put_env(:ymn, :ymn_module, YmnMock)
```


`test/ymn_test.exs`を修正
mockと出力すモックを定義します

```diff_elixir

defmodule YmnTest do
  use ExUnit.Case

+ import Mox
+ setup :verify_on_exit!

+ test "say" do
+   YmnMock
+   |> expect(:say, fn _ ->
+     IO.puts("mock")
+     :ok
+   end)

    get_ymn_module = YmnBehaviour.get_ymn_module()
    assert get_ymn_module.say("hoge") == :ok
  end
end
```

mix test実行してみます
Moxの内容に置き換えることができました
```
mix test
mock
.

Finished in 0.00 seconds (0.00s async, 0.00s sync)
1 test, 0 failures
```

通常の実行環境は、`lib/ymn.ex`の内容が実行されることが確認できました

```elixir
 iex -S mix
Erlang/OTP 24 [erts-12.3.1] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit]

==> mox
Compiling 3 files (.ex)
Generated mox app
==> ymn
Compiling 2 files (.ex)
Generated ymn app
Interactive Elixir (1.13.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> ymn_module = YmnBehaviour.get_ymn_module()
Ymn
iex(2)> ymn_module.say("hoge")
Hello!
hoge
:ok
```

# verify_on_exit!の実験

verify_on_exit!を追加時にモック(expect)と対象の関数呼ばれた回数が一致するか検証する

`test/ymn_test.exs`を修正
expectを追加


```diff_elixir

defmodule YmnTest do
  use ExUnit.Case

  import Mox
  setup :verify_on_exit!

  test "say" do
    YmnMock
    |> expect(:say, fn _ ->
      IO.puts("mock")
      :ok
    end)
+    |> expect(:say, fn _ ->
+      IO.puts("mock2")
+      :ok
+    end)

    get_ymn_module = YmnBehaviour.get_ymn_module()
    assert get_ymn_module.say("hoge") == :ok
  end
end
```

```
$ mix test
mock


  1) test say (YmnTest)
     test/ymn_test.exs:7
     ** (Mox.VerificationError) error while verifying mocks for #PID<0.219.0>:
     
       * expected YmnMock.say/1 to be invoked 2 times but it was invoked once
     stacktrace:
       (mox 1.0.1) lib/mox.ex:713: Mox.verify_mock_or_all!/3
       (ex_unit 1.13.0) lib/ex_unit/on_exit_handler.ex:143: ExUnit.OnExitHandler.exec_callback/1
       (ex_unit 1.13.0) lib/ex_unit/on_exit_handler.ex:129: ExUnit.OnExitHandler.on_exit_runner_loop/0



Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 test, 1 failure
```

モックと対象関数が呼ばれる回数が不一致の為エラーになりました

#### モックと対象関数が呼ばれる回数を合わせてみる


`test/ymn_test.exs`を修正
get_ymn_module.sayを追加

```diff_elixir
defmodule YmnTest do
  use ExUnit.Case

  import Mox
  setup :verify_on_exit!

  test "say" do
    YmnMock
    |> expect(:say, fn _ ->
      IO.puts("mock")
      :ok
    end)
    |> expect(:say, fn _ ->
      IO.puts("mock2")
      :ok
    end)

    get_ymn_module = YmnBehaviour.get_ymn_module()
    assert get_ymn_module.say("hoge") == :ok
+   assert get_ymn_module.say("hogehoge") == :ok
  end
end
```

```
$ mix test
mock
mock2
.

Finished in 0.00 seconds (0.00s async, 0.00s sync)
1 test, 0 failures

Randomized with seed 587642
```

モックと対象関数が呼ばれる回数が一致したため正常に終了しました