Elixir 1.8.0에 추가된 파라미터화 테스트(parameterized test)
같은 테스트를 다른 파라미터로 실행하고 싶은 상황이 있다. 예를 들어 key-value 저장소를 구현하는데, 똑같은 인터페이스를 제공하는 다른 두 구현체가 같은 동작을 하는지를 검증하고 싶다.
함수 안에서 루프를 돌며 각 구현체에 대해 테스트하면 되지 않을까? 그렇게 할 수도 있다. 다만 테스트 함수 전에 호출하는 setup 함수를 활용하지 못한다. 테스트 구조화를 도와줘서 실제 테스트 코드를 간결하게 만드는 setup 함수도 살리고 싶다.
이런 경우 사용할 수 있는 게 파라미터화 테스트(parameterized test)다. elixir 1.18에 표준 테스트 라이브러리인 ExUnit에 추가됐다.
테스트 대상
key-value 저장소 구현체가 두 개 있다면? 파라미터화 테스트하기 좋은 대상이다. Property-Based Testing(속성 기반 테스트)가 더 좋긴 하지만 Example-based Testing(예제 기반 테스트)으로 범위를 좁혀서 생각하자.
defmodule Store do
@callback start_link() :: {:ok, any()}
@callback put(any(), any(), any()) :: any()
@callback get(any(), any()) :: any()
end
defmodule AgentStore do
@behaviour Store
use Agent
def start_link do
Agent.start_link(fn -> %{} end)
end
def put(pid, key, value) do
Agent.update(pid, &Map.put(&1, key, value))
end
def get(pid, key) do
Agent.get(pid, &Map.get(&1, key))
end
end
defmodule EtsStore do
@behaviour Store
def start_link do
table = :ets.new(__MODULE__, [:set, :public])
{:ok, table}
end
def put(table, key, value) do
:ets.insert(table, {key, value})
end
def get(table, key) do
case :ets.lookup(table, key) do
[{^key, value}] -> value
[] -> nil
end
end
end
key-value 저장소를 agent로도 구현했고 ets로도 구현했다.
파라미터화 테스트가 있기 전
매크로를 사용해서 테스트 코드를 여러 벌 만드는 방식을 사용했다.
defmodule StoreTest do
use ExUnit.Case
for store <- [AgentStore, EtsStore] do
test "#{inspect(store)} read after write" do
{:ok, table} = unquote(store).start_link()
unquote(store).put(table, :user_id, 42)
assert unquote(store).get(table, :user_id) == 42
end
end
end
아니 for 가 왜 저기에 있어? elixir는 런타임 뿐만 아니라 매크로를 사용해 코드를 생성할 때도 사용할 수 있다. store 에 AgentStore, EtsStore 가 들어가는 테스트 코드가 각각 만들어진다.
setup을 사용하지 못한다. 테스트 환경 세팅 코드와 테스트코드가 같이 있다.
파라미터화 테스트 사용
parameterize 리스트 요소가 setup 인자로 들어간다. setup을 살릴 수 있고 매크로를 사용하지 않아도 되니 unquote 매크로가 사라졌다.
defmodule StoreTest do
use ExUnit.Case,
async: true,
parameterize: [
%{store: AgentStore},
%{store: EtsStore}
]
setup %{store: store} do
{:ok, table} = store.start_link()
{:ok, store: store, table: table}
end
test "read after write", %{store: store, table: table} do
store.put(table, :user_id, 42)
assert store.get(table, :user_id) == 42
end
end
마치며
파라미터화 테스트는 테스트 셋업 코드를 분리할 수 있는 setup 함수를 사용할 수 있어서 코드를 더 깔끔하게 구조화할 수 있다.
강력한 매크로를 사용해 다들 그럭저럭 극복하고 있어서일까? 추가된 시기가 늦은 감이 있다.