エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Mock パッケージを使った Elixir のテスト

Mock を使ったテストを覚えたので、そのメモです。

全容は GitHub に置いてあります。

リポジトリ名は test_with_mocks_ex ですが、中の Elixir のプロジェクト名 は fizz_buzz になってます。気をつけて。

プロジェクトで定義している唯一の関数 FizzBuzz.fizz_buzz/1 は、数値を与えると FizzBuzz します。 実態はコマンド fb を呼び出しています。fb は各自実装してください。

…というわけにもいかないので、fb コマンドの呼び出しをモックします。

パッケージの追加

"Mock" というパッケージを利用します。

mix.exs に依存関係を追加します。

  defp deps do
    [
      {:mock, "~>0.3"}
    ]
  end

パッケージの取得。

$ mix deps.get

テスト対象

テスト対象の関数はこのようになっています。

関数 FizzBuzz.fizz_buzz/1System.cmd/2 を使ってコマンド fb を実行し、結果を返します。

defmodule FizzBuzz do
  @moduledoc """
  Documentation for FizzBuzz.
  """

  @doc """
  外部コマンド `fb` を利用して FizzBuzz する。

  `fb` コマンドは各自実装してください。
  """
  def fizz_buzz(n) do
    {res, _} = System.cmd("fb", ["#{n}"])
    res
  end
end

テストを書く

モジュールの import

タイプする手間を減らすために、テスト対象のモジュール FizzBuzz と、モックを提供するモジュール Mock を import しておきます。

defmodule FizzBuzzTest do
  use ExUnit.Case

  import Mock
  import FizzBuzz

  ...
end

モックで囲って assert する

マクロ Mock.with_mock/4 はモックするモジュール、モックする関数、およびブロックを取り、モックする関数がブロック内から呼び出されたときに本来の関数に代わってマクロに与えた関数が呼び出されます。

  test "fizz_buzz 1" do
    with_mock System, cmd: fn "fb", ["1"] -> {"1", 0} end do
      assert fizz_buzz(1) == "1"
    end
  end
# 引数
1 モジュール System
2 オプション (ここでは省略)
3 モック [{:cmd, fn "fb", ["1"] -> {"1", 0} end}]
4 ブロック do
assert fizz_buzz(1) == "1"
end

モックはキーワードリストになっているため、[cmd: fn "fb", ["1"] -> {"1", 0} end] と書くことができます。さらに引数では括弧 [] を省略できるため、上記のようは記述になっています。

FizzBuzz.fizz_buzz(1) は内部で System.cmd("fb", ["1"]) という形で呼び出しているので、モックする関数でその形に合わせた引数と戻り値を用意しています。

複数のモックで囲って assert する

マクロ Mock.with_mocks/2 は、モックをリストにして複数定義できます。

なのですが。ここではモックできる関数を用意できていないので、リストの要素は一つになっています。

  test "fizz_buzz 3" do
    with_mocks [{System, [], [cmd: fn "fb", ["3"] -> {"Fizz", 0} end]}] do
      assert fizz_buzz(3) == "Fizz"
    end
  end

Mock.with_mocks/2 に与えているモックのリストの要素 {System, [], [cmd: fn "fb", ["3"] -> {"Fizz", 0} end]}Mock.with_mock/4 と同じ構成になっています。 ただ、引数でないので省略した記述ができないのでオプションの空リストやモックの括弧も記述しています。

setup でモックする

モックを setup で定義します。 Mock.setup_with_mocks/2 のモックのリストの書式は Mock.with_mocks/2 と同じです。

ブロックの戻り値の扱いは ExUnit.Callbacks.setup/1 と同じです。ですので特に返す値がない場合は :ok を返します。

コンテクスト変数を受け取れる ExUnit.Callbacks.setup/1 に対応する Mock.setup_with_mocks/3 もあります。

  describe "case 5" do
    setup_with_mocks [{System, [], [cmd: fn "fb", ["5"] -> {"Buzz", 0} end]}] do
      :ok
    end

    test "fizz_buzz 5" do
      assert fizz_buzz(5) == "Buzz"
    end
  end

test でモックする

テスト定義でモックを記述します。 モックの記述の書式は Mock.with_mock/4 と同じです。

こちらもコンテクスト変数を受け取れる Mock.test_with_mock/6 が用意されています。

  test_with_mock "fizz_buzz 9", System, cmd: fn "fb", ["9"] -> {"Fizz", 0} end do
    assert fizz_buzz(9) == "Fizz"
  end

呼び出されたことを確かめる

マクロ Mock.call/1 は、引数に与えたモックされた関数の呼び出しがあったばあいに真を返します。

  test "fizz_buzz 15" do
    mock = fn "fb", ["15"] -> {"FizzBuzz", 0} end

    with_mock System, cmd: mock do
      assert fizz_buzz(15) == "FizzBuzz"
      assert called(System.cmd("fb", ["15"]))
    end
  end

いつか読むはずっと読まない:三部作、再び

いままで読んできたシリーズものでも邦訳が完結してないのが多いんだよな…。「恐竜惑星(惑星アイリータ調査隊)」とか「ダーコーヴァ年代記」とか「ワイルド・カード」とか…。

「〈クロノス・クロニクル〉第一弾」?

…またシリーズものに手を出してしまった。

2011/01/26 のブログの記事より)

ボヘミアの不思議キャビネット (創元推理文庫)

ボヘミアの不思議キャビネット (創元推理文庫)

天球儀とイングランドの魔法使い (創元推理文庫)

天球儀とイングランドの魔法使い (創元推理文庫)

それから何年経っただろうか。〈クロノス・クロニクル〉第 3 巻はいまだ刊行されていない。

そして。

本書はパテルの小説の処女作であるが、二〇一五年三月に出版されたのち同年七月には続巻の Cities and Thrones が出版され、今年(二〇一七年)には本三部作の最終巻となる The Song of the Dead が出版された。

(本書のあとがきより)

…またシリーズものに手を出してしまった。

墓標都市 (創元SF文庫)

墓標都市 (創元SF文庫)