Skip to content

Instantly share code, notes, and snippets.

@kerryb
Last active May 19, 2019
Embed
What would you like to do?
Basic given/when/then macros for ExUnit

README

A spike looking at adding basic given-when-then steps to ExUnit tests.

Features

  • define tests as sequences of calls to given_, when_ and then_ (unfortunately when is a reserved word)
  • match steps by calling defwhen etc with a string matching the one used in the step
  • interpolate values into step descriptions using {braces}
    • placeholder variable names are available as methods on the magic args variable
  • pass data between steps by calling save and get with a key
    • data is stored in a per-test Agent, allowing tests to run in parallel
# This shouldn't really be in lib
#
defmodule FeatureCase do
defmacro __using__(options) do
quote do
use ExUnit.Case, unquote(options)
import FeatureCase
setup do
{:ok, _} = Agent.start(fn -> %{} end, name: FeatureCase.agent_name())
:ok
end
defp save(key, value) do
Agent.update(FeatureCase.agent_name(), fn state ->
Map.put(state, key, value)
end)
end
defp get(key) do
Agent.get(FeatureCase.agent_name(), fn state -> state[key] end)
end
def step(step) do
{label, args} = parse_step(step)
step(label, args)
end
defdelegate given_(step), to: __MODULE__, as: :step
defdelegate when_(step), to: __MODULE__, as: :step
defdelegate then_(step), to: __MODULE__, as: :step
end
end
def agent_name, do: {:global, {__MODULE__, :state, self()}}
defmacro defgiven(step, do: block), do: define_step(step, block)
defmacro defwhen(step, do: block), do: define_step(step, block)
defmacro defthen(step, do: block), do: define_step(step, block)
defp define_step(step, block) do
case FeatureCase.parse_step(step) do
{label, []} ->
quote do
def step(unquote(label), _values), do: unquote(block)
end
{label, var_names} ->
quote do
def step(unquote(label), values) do
var!(args) =
unquote(var_names)
|> Enum.map(&String.to_atom/1)
|> Enum.zip(values)
|> Enum.into(%{})
unquote(block)
end
end
end
end
def parse_step(step) do
with {vars, label_chunks} <-
step
|> String.split(~r/\{.*?\}/, include_captures: true, trim: true)
|> Enum.split_with(fn s -> s =~ ~r/\{.*\}/ end),
label <- label_chunks |> Enum.join("{}"),
args <- vars |> Enum.map(fn a -> String.replace(a, ~r/\{(.*)\}/, "\\1") end) do
{label, args}
end
end
end
defmodule FooTest do
use FeatureCase, async: true
test "Adding" do
given_ "a calculator"
when_ "I add {2} and {3}"
then_ "the result is {5}"
end
test "Adding again" do
given_ "a calculator"
when_ "I add {2} and {40}"
then_ "the result is {42}"
end
defgiven "a calculator" do
# this is a no-op
end
defwhen "I add {a} and {b}" do
save(:result, String.to_integer(args.a) + String.to_integer(args.b))
end
defthen "the result is {c}" do
assert get(:result) == String.to_integer(args.c)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment