Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,49 @@ reject(&Calculator.add/2)
assert_raise Mimic.UnexpectedCallError, fn -> Calculator.add(4, 2) end
```

### Calls

`calls/3` returns a list of args for each call to a stubbed Mimic function.

```elixir
defmodule Calculator do
def mult(x, y) do
x * y
end
end

Calculator
|> expect(:mult, fn x, y -> x + y end)

[] = calls(Calculator, :mult, 2)

9 = Calculator.mult(3, 3)

[[3, 3]] = calls(Calculator, :mult, 2)
```

`calls/1` works the same way, but with a capture of the function:

```elixir
defmodule Calculator do
def mult(x, y) do
x * y
end
end

Calculator
|> expect(:mult, fn x, y -> x + y end)

[] = calls(&Calculator.mult/2)

9 = Calculator.mult(3, 3)

[[3, 3]] = calls(&Calculator.mult/2)
```

When `calls` is called they are popped out of the list of calls. Next time `calls` is used it will only
return new calls since the last time that `calls` was used.

## Private and Global mode

The default mode is private which means that only the process
Expand Down
79 changes: 79 additions & 0 deletions lib/mimic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,85 @@ defmodule Mimic do
Server.get_mode()
end

@doc """
Get the list of calls made to a mocked/stubbed function.

This function returns a list of all arguments passed to the function during each call.
If the function has not been mocked/stubbed or has not been called, it will raise an error.

## Arguments:

* `function` - A capture of the function to get the calls for.

## Returns:

* A list of lists, where each inner list contains the arguments from one call.

## Raises:

* If the function has not been mocked/stubbed.
* If the function does not exist in the module.

## Example:

iex> Calculator.add(1, 2)
3
iex> Mimic.calls(&Calculator.add/2)
[[1, 2]]

"""
@spec calls(function) :: [[any]] | {:error, :atom}
def calls(function) do
fun_info = Function.info(function)
module = fun_info[:module]
fn_name = fun_info[:name]
arity = fun_info[:arity]

calls(module, fn_name, arity)
end

@doc """
Get the list of calls made to a mocked/stubbed function.

This function returns a list of all arguments passed to the function during each call.
If the function has not been mocked/stubbed or has not been called, it will raise an error.

## Arguments:

* `module` - the name of the module containing the function.
* `function_name` - the name of the function.
* `arity` - the arity of the function.

## Returns:

* A list of lists, where each inner list contains the arguments from one call.

## Raises:

* If the function has not been mocked/stubbed.
* If the function does not exist in the module.

## Example:

iex> Calculator.add(1, 2)
3
iex> Mimic.calls(Calculator, :add, 2)
[[1, 2]]

"""
@spec calls(module, atom, non_neg_integer) :: [[any]] | {:error, :atom}
def calls(module, function_name, arity) do
raise_if_not_exported_function!(module, function_name, arity)

result =
Server.get_calls(module, function_name, arity)
|> validate_server_response(:calls)

with {:ok, calls} <- result do
calls
end
end

defp ensure_module_not_copied(module) do
case Server.marked_to_copy?(module) do
false -> :ok
Expand Down
66 changes: 62 additions & 4 deletions lib/mimic/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ defmodule Mimic.Server do
modules_beam: %{},
modules_to_be_copied: MapSet.new(),
reset_tasks: %{},
modules_opts: %{}
modules_opts: %{},
call_history: %{}
end

defmodule Expectation do
Expand Down Expand Up @@ -106,6 +107,11 @@ defmodule Mimic.Server do
GenServer.call(__MODULE__, {:marked_to_copy?, module}, @long_timeout)
end

@spec get_calls(module, atom, arity) :: {:ok, list(list(term))} | {:error, :not_found}
def get_calls(module, fn_name, arity) do
GenServer.call(__MODULE__, {:get_calls, {module, fn_name, arity}, self()})
end

def apply(module, fn_name, args) do
arity = Enum.count(args)
original_module = Mimic.Module.original(module)
Expand All @@ -126,7 +132,7 @@ defmodule Mimic.Server do
end

defp do_apply(owner_pid, module, fn_name, arity, args) do
case GenServer.call(__MODULE__, {:apply, owner_pid, module, fn_name, arity}, :infinity) do
case GenServer.call(__MODULE__, {:apply, owner_pid, module, fn_name, arity, args}, :infinity) do
{:ok, func} ->
Kernel.apply(func, args)

Expand Down Expand Up @@ -221,7 +227,9 @@ defmodule Mimic.Server do
state
end

%{state | expectations: expectations, stubs: stubs}
call_history = Map.delete(state.call_history, pid)

%{state | expectations: expectations, stubs: stubs, call_history: call_history}
end

defp find_stub(stubs, module, fn_name, arity, caller) do
Expand All @@ -231,7 +239,27 @@ defmodule Mimic.Server do
end
end

def handle_call({:apply, owner_pid, module, fn_name, arity}, _from, state) do
defp get_call_history(state, caller, module, fn_name, arity) do
get_in(state.call_history, [Access.key(caller, %{}), {module, fn_name, arity}])
end

defp put_call_history(state, caller, module, fn_name, arity, args) do
call_history = get_call_history(state, caller, module, fn_name, arity) || []

%{
state
| call_history:
put_in(
state.call_history,
[Access.key(caller, %{}), {module, fn_name, arity}],
[
args | call_history
]
)
}
end

def handle_call({:apply, owner_pid, module, fn_name, arity, args}, _from, state) do
caller =
if state.mode == :private do
owner_pid
Expand All @@ -246,6 +274,9 @@ defmodule Mimic.Server do
expectations =
put_in(state.expectations, [caller, {module, fn_name, arity}], new_expectations)

# Track call history
state = put_call_history(state, caller, module, fn_name, arity, args)

{:reply, {:ok, func}, %{state | expectations: expectations}}

{:unexpected, num_calls, num_applied_calls} ->
Expand All @@ -258,6 +289,9 @@ defmodule Mimic.Server do
{:reply, :original, state}

{:ok, func} ->
# Track call history for stubs too
state = put_call_history(state, caller, module, fn_name, arity, args)

{:reply, {:ok, func}, state}
end
end
Expand Down Expand Up @@ -505,6 +539,30 @@ defmodule Mimic.Server do
end
end

def handle_call({:get_calls, {module, fn_name, arity}, owner_pid}, _from, state) do
caller_pids = [self() | Process.get(:"$callers", [])]

caller_pid =
case allowed_pid(caller_pids, module) do
{:ok, owner_pid} -> owner_pid
_ -> owner_pid
end

case ensure_module_copied(module, state) do
{:ok, state} ->
case pop_in(state.call_history, [Access.key(caller_pid, %{}), {module, fn_name, arity}]) do
{calls, call_history} when is_list(calls) ->
{:reply, {:ok, Enum.reverse(calls)}, %{state | call_history: call_history}}

{nil, _} ->
{:reply, {:ok, []}, state}
end

{:error, reason} ->
{:reply, {:error, reason}, state}
end
end

defp maybe_typecheck_func(module, fn_name, func) do
case module.__mimic_info__() do
{:ok, %{type_check: true}} ->
Expand Down
130 changes: 130 additions & 0 deletions test/mimic_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,4 +1073,134 @@ defmodule Mimic.Test do
assert to_string(s) == "{abc} - {def}"
end
end

describe "calls/1" do
setup :set_mimic_private

test "returns calls for stubbed functions" do
stub(Calculator, :add, fn x, y -> x + y end)

Calculator.add(1, 2)
Calculator.add(3, 4)

assert Mimic.calls(&Calculator.add/2) == [[1, 2], [3, 4]]
assert Mimic.calls(&Calculator.add/2) == []
end
end

describe "calls/3 private mode" do
setup :set_mimic_private

test "returns calls for stubbed functions" do
stub(Calculator, :add, fn x, y -> x + y end)

Calculator.add(1, 2)
Calculator.add(3, 4)

assert Mimic.calls(Calculator, :add, 2) == [[1, 2], [3, 4]]
assert Mimic.calls(Calculator, :add, 2) == []
end

test "returns calls for expected functions" do
expect(Calculator, :add, 2, fn x, y -> x + y end)

Calculator.add(1, 2)
Calculator.add(3, 4)

assert Mimic.calls(Calculator, :add, 2) == [[1, 2], [3, 4]]
assert Mimic.calls(Calculator, :add, 2) == []
end

test "return calls from child pid as well" do
parent_pid = self()

Calculator
|> expect(:add, fn _, _ -> @expected end)
|> stub(:mult, fn _, _ -> @stubbed end)

spawn_link(fn ->
Calculator
|> allow(parent_pid, self())

assert Calculator.add(1, 2) == @expected
assert Calculator.mult(3, 4) == @stubbed
send(parent_pid, :ok)
end)

assert_receive :ok
assert Mimic.calls(&Calculator.add/2) == [[1, 2]]
assert Mimic.calls(&Calculator.add/2) == []

assert Mimic.calls(&Calculator.mult/2) == [[3, 4]]
assert Mimic.calls(&Calculator.mult/2) == []
end

test "raises when mock is not defined" do
assert_raise ArgumentError, fn -> Mimic.calls(Date, :add, 2) end
end

test "raises for non-existent functions" do
assert_raise ArgumentError,
"Function invalid/2 not defined for Calculator",
fn -> Mimic.calls(Calculator, :invalid, 2) end
end

test "raises for non-existent modules" do
assert_raise ArgumentError, "Function add/2 not defined for NonExistentModule", fn ->
Mimic.calls(NonExistentModule, :add, 2)
end
end
end

describe "calls/3 global mode" do
setup :set_mimic_global

test "returns calls for stubbed functions" do
stub(Calculator, :add, fn x, y -> x + y end)

parent_pid = self()

spawn_link(fn ->
Calculator.add(1, 2)
Calculator.add(3, 4)
send(parent_pid, :ok)
end)

assert_receive :ok
assert Mimic.calls(Calculator, :add, 2) == [[1, 2], [3, 4]]
assert Mimic.calls(Calculator, :add, 2) == []
end

test "returns calls for expected functions" do
expect(Calculator, :add, 2, fn x, y -> x + y end)

parent_pid = self()

spawn_link(fn ->
Calculator.add(1, 2)
Calculator.add(3, 4)
send(parent_pid, :ok)
end)

assert_receive :ok
assert Mimic.calls(Calculator, :add, 2) == [[1, 2], [3, 4]]
assert Mimic.calls(Calculator, :add, 2) == []
end

test "raises when mock is not defined" do
assert_raise ArgumentError, fn -> Mimic.calls(Date, :add, 2) end
end

test "raises for non-existent functions" do
assert_raise ArgumentError,
"Function invalid/2 not defined for Calculator",
fn -> Mimic.calls(Calculator, :invalid, 2) end
end

test "raises for non-existent modules" do
assert_raise ArgumentError, "Function add/2 not defined for NonExistentModule", fn ->
Mimic.calls(NonExistentModule, :add, 2)
end
end
end
end
Loading