Created
March 16, 2018 11:45
-
-
Save teamon/bad0891c02c26ed774890e04bb998e15 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Twin do | |
@moduledoc """ | |
See http://teamon.eu/2017/different-approach-to-elixir-mocks-doubles/ | |
""" | |
## PROXY | |
defmodule Proxy do | |
def unquote(:"$handle_undefined_function")(fun, args) do | |
[{__MODULE__, mod} | rest] = Enum.reverse(args) | |
args = Enum.reverse(rest) | |
case Twin.call(mod, fun, args) do | |
{:ok, ret} -> ret | |
{:error, :nostub} -> apply(mod, fun, args) | |
end | |
end | |
end | |
## MACROS | |
def assert_called(mod, fun) do | |
ExUnit.Assertions.assert Twin.called?(mod, fun), "#{mod}.#{fun} was not called" | |
end | |
def assert_called(mod, fun, args) do | |
case Twin.called?(mod, fun, args) do | |
:ok -> :ok | |
{:error, %{history: history}} -> | |
history = | |
history | |
|> Enum.map(fn {_,f,a} -> "#{f}(#{format_args(a)})" end) | |
|> Enum.join("\n") | |
msg = """ | |
#{mod}.#{fun}(#{format_args(args)}) was NOT called | |
Recorded calls to module #{mod}: | |
#{history} | |
""" | |
raise ExUnit.AssertionError, message: msg | |
end | |
end | |
def refute_called(mod, fun) do | |
ExUnit.Assertions.refute Twin.called?(mod, fun), "#{mod}.#{fun} was called" | |
end | |
def refute_called(mod, fun, args) do | |
ExUnit.Assertions.refute Twin.called?(mod, fun, args), | |
"#{mod}.#{fun}(#{args |> Enum.map(&inspect/1) |> Enum.join(", ")}) was called" | |
end | |
def verify_stubs do | |
stubs = Twin.stubs | |
ExUnit.Assertions.assert stubs == [], | |
"Following stubs were not called:\n#{stubs |> Enum.map(&inspect/1) |> Enum.join("\n")}" | |
end | |
defp format_args(args), do: args |> Enum.map(&inspect/1) |> Enum.join(", ") | |
use GenServer | |
## CLIENT API | |
def start, do: GenServer.start(__MODULE__, [], name: __MODULE__) | |
def call(mod, fun, args), do: GenServer.call(__MODULE__, {:call, {mod, fun, args}}) | |
def called?(mod, fun), do: GenServer.call(__MODULE__, {:called?, {mod, fun}}) | |
def called?(mod, fun, args), do: GenServer.call(__MODULE__, {:called?, {mod, fun, args}}) | |
def stubs(pid \\ self()), do: GenServer.call(__MODULE__, {:stubs, pid}) | |
def stub(pid \\ self(), mod, fun, ret) do | |
if apply(mod, :__info__, [:functions])[fun] do | |
GenServer.call(__MODULE__, {:stub, pid, {mod, fun, ret}}) | |
mod | |
else | |
raise ExUnit.AssertionError, message: "Module #{mod} does not export function #{fun}" | |
end | |
end | |
def get(mod) do | |
case Mix.env do | |
:test -> {Twin.Proxy, mod} | |
_ -> mod | |
end | |
end | |
## CALLBACKS | |
def init(_) do | |
{:ok, %{}} | |
end | |
def handle_call({:call, mfa}, {pid, _}, state) do | |
{ret, dict} = do_call(state[pid], mfa) | |
{:reply, ret, Map.put(state, pid, dict)} | |
end | |
def handle_call({:stub, pid, mfr}, _, state) do | |
dict = do_stub(state[pid], mfr) | |
{:reply, :ok, Map.put(state, pid, dict)} | |
end | |
def handle_call({:called?, mfa}, {pid, _}, state) do | |
if do_called?(state[pid], mfa) do | |
{:reply, :ok, state} | |
else | |
{:reply, {:error, state[pid]}, state} | |
end | |
end | |
def handle_call({:stubs, pid}, _, state) do | |
{:reply, get_in(state, [pid, :stubs]) || [], state} | |
end | |
## INTERNALS | |
defp do_call(nil, mfa) do | |
{{:error, :nostub}, %{stubs: [], history: [mfa]}} | |
end | |
defp do_call(%{stubs: stubs, history: history}, {m,f,_} = mfa) do | |
# check for stubs, else pass-through | |
{ret, stubs} = case find_stub(stubs, {m,f}) do | |
{nil, stubs} -> {{:error, :nostub}, stubs} | |
{ret, stubs} -> {{:ok, ret}, stubs} | |
end | |
# save call to history | |
{ret, %{stubs: stubs, history: [mfa | history]}} | |
end | |
defp do_stub(nil, mfr), do: %{stubs: [mfr], history: []} | |
defp do_stub(dict, mfr), do: %{dict | stubs: dict.stubs ++ [mfr]} | |
defp do_called?(nil, _), do: false | |
defp do_called?(%{history: history}, {m,f}), do: Enum.find(history, &match?({^m, ^f, _}, &1)) != nil | |
defp do_called?(%{history: history}, {m,f,a}), do: Enum.find(history, &match?({^m, ^f, ^a}, &1)) != nil | |
defp find_stub(xs, mf), do: find_stub(xs, mf, []) | |
defp find_stub([], _, rest), do: {nil, Enum.reverse(rest)} | |
defp find_stub([{m,f,r} | xs], {m,f}, rest), do: {r, Enum.reverse(rest) ++ xs} | |
defp find_stub([x | xs], mf, rest), do: find_stub(xs, mf, [x | rest]) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment