Elixir で Parameterized test

2019/12/09 12:00am

この記事は以前に Qiita で公開した記事です。

ユニットテストの現場では、一部のパラメータだけが異なるコピペが見過ごされがちで、コードを保守する立場にとってはテスト対象のコードよりも頭が痛い存在だ。こういうテストは異なるパラメータのパターンのみを記述して、他のコードは使いまわしたい。いわゆる Parameterized test というやつである。

既存の手法

Elixir の ExUnit は標準の Unit Testing ライブラリとして非常によく出来ているが、残念ながら Parameterized test の仕組みは用意されていない。

KazuCocoa/ex_parameterized を試してみたこともあるのだが、記法が変則的(こうなる理由も分かる)なのと、パラメータを評価する部分で動かないコードがあるので採用を見送っている。

いま採用している手法

結局、いろいろ試した末、いまはこのようなコードに落ち着いている。

# 1. The helper functions for the test module. To make it possible to import
# this helper module in the test module, define this module outside the context that uses it.
defmodule MyTest.Helpers do
  @spec fake_params(Enumrable.t()) :: map
  def fake_params(override \\ %{}) do
    %{
      country: "jp",
      phone_number: Faker.phone_number(),
      locale: "ja",
      company: "My Company",
      department: "My Department",
      email: Faker.Internet.email(),
      first_name: Faker.Name.first_name(),
      last_name: Faker.Name.last_name()
    }
    |> Map.merge(Map.new(override))
  end
end

defmodule MyTest do
  use MyApp.ConnCase

  # Because I'd like to use functions in the helper module both in parameterized cases and
  # test cases, alias and import it.
  alias MyTest.Helpers
  import Helpers

  describe "signup" do
    for {description, signup_params} <- [
          # 2. You cannot invoke functions in the testing module which is not defined yet.
          # So we need the helper module.
          "all filled": Helpers.fake_params(),
          "department can be omitted": Helpers.fake_params(department: nil),
          "department can be null": Helpers.fake_params() |> Map.delete("department")
        ] do
      # 3. You cannot use variables in this context in the context inside a test case.
      # So you have to use module attributes or `@tag` feature in ExUnit. Personally,
      # I prefer the latter.
      @tag signup_params: signup_params
      test "no errors: #{description}", %{conn: conn, signup_params: signup_params} do
        # ...
      end
    end
  end
end

なんでこうなってるかはコメントの通りで紆余曲折あるのだが、

  1. ヘルパー関数を定義するモジュールはテストのモジュールの外側で定義する。なぜなら、そうしないと import できないから。もちろん、import しなくてもいいのだが、テストコードからノイズはできるだけ減らしたい
  2. そもそも、なんでヘルパー関数をプライベートな関数ではなく、わざわざ別のモジュールに定義しているか、というと、パラメータのパターンを生成するときに使いたいから。この時点ではテストのモジュールは定義されていない
  3. 少々トリッキーなのは test を記述する部分。test do ... end の内部では、外部の変数が見えないことに注意

すこし冗長な書き方にはなるが、はじめて見る人でも「ループで異なるパターンのテストを書いてる」感が伝わるのは重要だと思う。