ユニットテストの現場では、一部のパラメータだけが異なるコピペが見過ごされがちで、コードを保守する立場にとってはテスト対象のコードよりも頭が痛い存在だ。こういうテストは異なるパラメータのパターンのみを記述して、他のコードは使いまわしたい。いわゆる 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
なんでこうなってるかはコメントの通りで紆余曲折あるのだが、
- ヘルパー関数を定義するモジュールはテストのモジュールの外側で定義する。なぜなら、そうしないと
import
できないから。もちろん、import
しなくてもいいのだが、テストコードからノイズはできるだけ減らしたい - そもそも、なんでヘルパー関数をプライベートな関数ではなく、わざわざ別のモジュールに定義しているか、というと、パラメータのパターンを生成するときに使いたいから。この時点ではテストのモジュールは定義されていない
- 少々トリッキーなのは
test
を記述する部分。test do ... end
の内部では、外部の変数が見えないことに注意
すこし冗長な書き方にはなるが、はじめて見る人でも「ループで異なるパターンのテストを書いてる」感が伝わるのは重要だと思う。