LoginSignup
2

More than 3 years have passed since last update.

Elixir で Parameterized test

Last updated at Posted at 2019-12-09

個人ブログにも転載しています

ユニットテストの現場では、一部のパラメータだけが異なるコピペが見過ごされがちで、コードを保守する立場にとってはテスト対象のコードよりも頭が痛い存在だ。こういうテストは異なるパラメータのパターンのみを記述して、他のコードは使いまわしたい。いわゆる 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 の内部では、外部の変数が見えないことに注意

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2