Moxを試してみる

はじめに

MoxはElixirのMockライブラリです。最近はElixirでコードを書く機会が増えているのいて、Mockをどうすれば良いのかよくわかってなかったのですが、手近なプロジェクトを覗いて見たらMoxというMockライブラリを使っていました。
Elixir界隈のデファクトみたいなのが掴みきれていないところはあるのですが。検索でもMoxが多く引っかかるので、このライブラリについて学びたいと思います。
このブログではその使い方について、まとめおこうと思います。

  • Moxの基本的な使い方

Mockするな?

本筋とは少しそれるのですが、Elixir SchoolにMockについて少し気になることがかかれていました。

Elixirでのモックに対する単純な解答は、使うな、です

なるほど?と思いながらその意図について書かれた議論を読んでみるとおおよそ以下のようなことが書かれていた。

  • 単語であるMock動詞としてではなく、名詞として使おう
  • ビヘイビアでAPIの規約をきちんと決めてMockを作ろう

日本語に直すとかなり混乱しますが、要は「Mockする(動詞)のではなくビヘイビアできちんとAPIの規約を定義して、その定義を利用したMock(名詞)を作り利用せよ」と言うことみたいです。
(若干、理解がまだ甘いところがありそうなので、ご指摘等があればコメントいただけますとありがたいです)

Moxを使ってみる

前置きはさておき、Moxを使っていきます。

動作環境

このブログの内容を実施した環境は次のとおりです。

$ uname -srvmpio
Linux 5.3.0-46-generic #38~18.04.1-Ubuntu SMP Tue Mar 31 04:17:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

$ elixir -v
Erlang/OTP 22 [erts-10.7.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.10.2 (compiled with Erlang/OTP 21)

下準備

Projectを作成し、Moxの依存を追加する

まずはMoxを使うための諸々の準備をしていきます。
以下のコマンドでMixプロジェクトを作成します。

$ mix new hello_mox

次に hello_mox/mix.exsに依存を追加します

  defp deps do
    [
      # 依存を追加
      {:mox, "~> 0.5", only: :test}
    ]
  end

プロジェクトのルートディレクトリに移動し、 mix deps.getコマンドで依存ライブラリを取得しておきます。 (今後出てくるコマンドはすべてプロジェクトのルートで実施することを想定しています)

$ mix deps.get

これで、Moxのセットアップは終わりです。

テストするモジュールを用意しておく

実際にMoxを使う前に下準備としてテスト対象のモジュールを作成しておきましょう。

lib/service.exs

defmodule Service.GreetingBehaviour do
  @callback greeting() :: charlist()
end

defmodule Service.GreetingJapanese do
  @behaviour Service.GreetingBehaviour
  def greeting() do
    'こんにちは'
  end
end

service.exにはService.GreetingBehaviourというビヘイビアとその実装を定義しています。
実装であるService.GreetingJapaneseではこんにちわという文字列を返すだけの簡単な関数を用意しておきます。

次に用意した関数を呼び出すモジュールを作ります。

lin/hello_mox.ex

defmodule HelloMox do
  @service Application.get_env(:hello_mox, :greeting)

  def run() do
   IO.puts  @service.greeting()
  end
end

このモジュールはconfg.exs経由でDIされたモジュールを呼び出し標準出力に出力するだけの簡単なモジュールです。

まだ、config.esxは存在していないのでこのあとすぐ作ります。

configを用意する

まず、注意点ですが、elixirの1.9以降で、mix newで作成するプロジェクトにデフォルトでconfig/config.exsが作成されないようになっているみたいです。ライブラリを作成する際に、configに依存することは望ましくないとされているからです。
これは、configがグローバルな環境変数として登録されてしまうからで、ライブラリの修正等を行なう際にその自由度を下げてしまいます。 (詳細はここを参照してください)

今回に限って言えば、ただ、Moxの機能を試したいだけなので、config/config.exsを作成しちゃいます。

$ mkdir config
$ echo "import Mix.Config" >> config/config.exs

作成したconfig/config.exsを開き以下のように編集しておきます。

import Config

# 実行環境に対応したコンフィグを読み込む
import_config "#{Mix.env()}.exs"

また、実行時に読み込むコンフィグであるdev.exsも作成しておきます。

config/dev.exs

import Config

config :hello_mox,
       greeting: Service.GreetingJapanese

これで、HelloMox.run実行時にService.GreetingJapaneseがインジェクションされるようになりました。
実際にHelloMox.run実行してみましょう。

$ mix run -e "HelloMox.run"
Compiling 2 files (.ex)
Generated hello_mox app
こんにちは

想定通りに動いてそうですね。

Moxを使ってMockを作成してみる

下準備が終わったので、いよいよMoxを使っていきたいと思います。
まずはMockの定義を書いてみます。
Mockの定義は通常test_helper.exsに書かれることが多いようです。

test/test_helper.exs

ExUnit.start()

#Mockの定義を記述する。ビヘイビアに対して`Service.GreetingMock`を定義している
Mox.defmock(Service.GreetingMock, for:  Service.GreetingBehaviour)

次にconfigを作成します。

config/text.exs

import Config

config :hello_mox,
       greeting: Service.GreetingMock

これで実行環境がテストの場合はHelloMox.run実行時にService.GreetMockがインジェクとされるようになります。

最後にテストコードを記述します

defmodule HelloMoxTest do
  import Mox
  use ExUnit.Case
  doctest HelloMox

  test "Moxでもっくをりようしてみるテスト" do
    Service.GreetingMock
    |> expect(:greeting, fn () -> 'Hello, Mox' end)

    HelloMox.run()
  end
end

import Moxをインポートし、

Service.GreetingMock
    |> expect(:greeting, fn () -> 'Hello, Mox' end)

の部分でMockの挙動を定義しています。
expectは引数としてMockする関数名のアトムと関数を引数として受け取ります。関数はHello, Moxをの文字列を返します。
それではテストを実行してみます。

$ mix test
Hello, Mox
.

Finished in 0.01 seconds
1 test, 0 failures

作成したMockのがインジェクとされ標準出力にHello, Moxが出力されるのを確認することができました。

感想

Mox自体の使い方はさほど難しくありませんでしたが、Mixプロジェクトの周りで結構ハマりました。

参考資料