Repository: edgurgel/mimic
Pull request #3 (2018-09-16 22:08:54)

Update the docs again.
After Eduardo's release I've gone back and fixed that conflicts and opened a new PR.


Patch:
diff --git a/.travis.yml b/.travis.yml
index f2bbfc7..0218d60 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,3 +9,4 @@ otp_release:
 script:
   - mix test
   - mix format --check-formatted
+  - mix credo --strict
diff --git a/lib/mimic.ex b/lib/mimic.ex
index 1483756..8c46c68 100644
--- a/lib/mimic.ex
+++ b/lib/mimic.ex
@@ -1,62 +1,119 @@
 defmodule Mimic do
   @moduledoc """
-  Mimic is a library that simplifies the usage of mocks.
+  Mimic is a library that simplifies the usage of mocks in Elixir.
 
-  Modules need to be prepared so that they can be used.
+  Mimic is mostly API compatible with [mox](https://hex.pm/packages/mox) but
+  doesn't require explicit contract checking with behaviours.  It's also faster.
+  You're welcome.
 
-  You must first call `copy` in your `test_helper.exs` for
-  each module that may have the behaviour changed.
+  Mimic works by copying your module out of the way and replacing it with one of
+  it's own which can delegate calls back to the original or to a mock function
+  as required.
 
-  ```
-  Mimic.copy(Calculator)
+  In order to prepare a module for mocking you must call `copy/1` with the
+  module as an argument.  We suggest that you do this in your
+  `test/test_helper.exs`:
 
+  ```elixir
+  Mimic.copy(Calculator)
   ExUnit.start()
   ```
 
-  Calling `copy` will not change the behaviour of the module.
+  Importantly calling `copy/1` will not change the behaviour of the module. When
+  writing tests you can then use `stub/3` or `expect/3` to add mocks and
+  assertions.
 
-  The user must call `stub/3` or `expect/3` so that the functions will
-  behave differently.
-  """
-  alias Mimic.Server
+  ## Multi-process collaboration
 
-  defmodule UnexpectedCallError do
-    defexception [:message]
-  end
+  Mimic supports multi-process collaboration via two mechanisms:
 
-  defmodule VerificationError do
-    defexception [:message]
-  end
+    1. Explicit allows.
+    2. Global mode.
 
-  defmodule Error do
-    defexception ~w(module fn_name arity)a
+  Using explicit allows is generally preferred as these stubs can be run
+  concurrently, whereas global mode tests must be run exclusively.
 
-    def message(e) do
-      mfa = Exception.format_mfa(e.module, e.fn_name, e.arity)
-      "#{mfa} cannot be stubbed as original module does not export such function"
-    end
+  ## Explicit allows
+
+  Using `allow/3` you can give other processes permission to use stubs and
+  expectations from where they were not defined.
+
+  ```elixir
+  test "invokes add from a task" do
+    Caculator
+    |> expect(:add, fn x, y -> x + y end)
+
+    parent_pid = self()
+
+    Task.async(fn ->
+      Calculator |> allow(parent_pid, self())
+      assert Calculator.add(2, 3) == 5
+    end)
+    |> Task.await
   end
+  ```
+
+  ## Global mode
+
+  When set in global mode any process is able to call the stubs and expectations
+  defined in your tests.
+
+  **Warning: Using global mode disables `async: true` in your tests**
+
+  Enable global mode using `set_mimic_global/1`.
 
-  use Application
+  ```elixir
+  setup :set_mimic_global
+  setup :verify_on_exit!
 
-  def start(_, _) do
-    children = [Server]
-    Supervisor.start_link(children, name: Mimic.Supervisor, strategy: :one_for_one)
+  test "invokes add from a task" do
+    Calculator
+    |> expect(:add, fn x, y -> x + y end)
+
+    Task.async(fn ->
+      assert Calculator.add(2, 3) == 5
+    end)
+    |> Task.await
   end
+  ```
+
+  The global mode must always be explicitly set per test. By default mocks run
+  in private mode.
+  """
+  alias ExUnit.Callbacks
+  alias Mimic.{Server, VerificationError}
 
   @doc """
-  To allow `Calculator.add/2` to be called:
+  Define a stub function for a copied module.
 
-    stub(Calculator, :add, fn x, y -> x + y end)
+  ## Arguments:
+
+    * `module` - the name of the module in which we're adding the stub.
+    * `function_name` - the name of the function we're stubbing.
+    * `function` - the function to use as a replacement.
+
+  ## Raises:
+
+    * If `module` is not copied.
+    * If `function_name` is not publicly exported from `module` with the same arity.
+
+  ## Example
+
+        iex> Calculator.add(2, 4)
+        6
+
+        iex> Mimic.stub(Calculator, :add, fn x, y -> x * y end)
+        ...> Calculator.add(2, 4)
+        8
 
   """
-  @spec stub(atom, atom, function) :: module
-  def stub(module, fn_name, func) do
-    arity = :erlang.fun_info(func)[:arity]
-    raise_if_not_mocked!(module)
-    raise_if_not_exported_function!(module, fn_name, arity)
+  @spec stub(module(), atom(), function()) :: module
+  def stub(module, function_name, function) do
+    arity = :erlang.fun_info(function)[:arity]
+    raise_if_not_copied!(module)
+    raise_if_not_exported_function!(module, function_name, arity)
 
-    case Server.stub(module, fn_name, arity, func) do
+    case Server.stub(module, function_name, arity, function) do
       :ok ->
         module
 
@@ -66,8 +123,31 @@ defmodule Mimic do
     end
   end
 
+  @doc """
+  Replace all public functions in `module` with stubs.
+
+  The stubbed functions will raise if they are called.
+
+  ## Arguments:
+
+    * `module` - The name of the module to stub.
+
+  ## Raises:
+
+    * If `module` is not copied.
+
+  ## Example
+
+      iex> Mimic.stub(Calculator)
+      ...> Calculator.add(2, 4)
+      ** (ArgumentError) Module Calculator has not been copied.  See docs for Mimic.copy/1
+          (mimic) lib/mimic.ex:129: Mimic.raise_if_not_copied!/1
+          (mimic) lib/mimic.ex:92: Mimic.stub/1
+
+  """
+  @spec stub(module()) :: module()
   def stub(module) do
-    raise_if_not_mocked!(module)
+    raise_if_not_copied!(module)
 
     case Server.stub(module) do
       :ok ->
@@ -80,11 +160,33 @@ defmodule Mimic do
   end
 
   @doc """
-  To expect `Calculator.add/2` to be called:
+  Define a stub which must be called within an example.
 
-    expect(Calculator, :add, fn x, y -> x + y end)
+  This function is almost identical to `stub/3` except that the replacement
+  function must be called within the lifetime of the calling `pid` (i.e. the
+  test example).
 
-  If this function is not called the verification step will raise
+  ## Arguments:
+
+    * `module` - the name of the module in which we're adding the stub.
+    * `function_name` - the name of the function we're stubbing.
+    * `function` - the function to use as a replacement.
+
+  ## Raises:
+
+    * If `module` is not copied.
+    * If `function_name` is not publicly exported from `module` with the same
+      arity.
+    * If `function` is not called by the stubbing process.
+
+  ## Example
+
+        iex> Calculator.add(2, 4)
+        6
+
+        iex> Mimic.expect(Calculator, :add, fn x, y -> x * y end)
+        ...> Calculator.add(2, 4)
+        8
   """
   @spec expect(atom, atom, non_neg_integer, function) :: module
   def expect(module, fn_name, num_calls \\ 1, func)
@@ -97,7 +199,7 @@ defmodule Mimic do
       when is_atom(module) and is_atom(fn_name) and is_integer(num_calls) and num_calls >= 1 and
              is_function(func) do
     arity = :erlang.fun_info(func)[:arity]
-    raise_if_not_mocked!(module)
+    raise_if_not_copied!(module)
     raise_if_not_exported_function!(module, fn_name, arity)
 
     case Server.expect(module, fn_name, arity, num_calls, func) do
@@ -123,7 +225,7 @@ defmodule Mimic do
     arity = fun_info[:arity]
     module = fun_info[:module]
     fn_name = fun_info[:name]
-    raise_if_not_mocked!(module)
+    raise_if_not_copied!(module)
     raise_if_not_exported_function!(module, fn_name, arity)
 
     case Server.expect(module, fn_name, arity, 0, func) do
@@ -136,27 +238,42 @@ defmodule Mimic do
     end
   end
 
-  defp raise_if_not_mocked!(module) do
-    unless function_exported?(module, :__mimic_info__, 0) do
-      raise ArgumentError, "Module #{inspect(module)} not mocked"
-    end
-  end
+  @doc """
+  Allow other processes to share expectations and stubs defined by another
+  process.
 
-  defp raise_if_not_exported_function!(module, fn_name, arity) do
-    unless function_exported?(module, fn_name, arity) do
-      raise ArgumentError, "Function #{fn_name}/#{arity} not defined for #{inspect(module)}"
-    end
-  end
+  ## Arguments:
 
-  @doc """
-  Allows other processes to share expectations and stubs defined by another process.
+    * `module` - the copied module.
+    * `owner_pid` - the process ID of the process which created the stub.
+    * `allowed_pid` - the process ID of the process which should also be allowed
+      to use this stub.
+
+  ## Raises:
+
+    * If Mimic is running in global mode.
+
+  Allows other processes to share expectations and stubs defined by another
+  process.
 
-  ## Examples
-  To allow `other_pid` to call any stubs or expectations defined for `Calculator`:
+  ## Example
 
-      allow(Calculator, self(), other_pid)
+  ```elixir
+  test "invokes add from a task" do
+    Caculator
+    |> expect(:add, fn x, y -> x + y end)
 
+    parent_pid = self()
+
+    Task.async(fn ->
+      Calculator |> allow(parent_pid, self())
+      assert Calculator.add(2, 3) == 5
+    end)
+    |> Task.await
+  end
+  ```
   """
+  @spec allow(module(), pid(), pid()) :: module() | {:error, atom()}
   def allow(module, owner_pid, allowed_pid) do
     case Server.allow(module, owner_pid, allowed_pid) do
       :ok ->
@@ -168,9 +285,14 @@ defmodule Mimic do
   end
 
   @doc """
-  Define `module` to be able to mock functions
+  Prepare `module` for mocking.
+
+  ## Arguments:
+
+    * `module` - the name of the module to copy.
+
   """
-  @spec copy(atom) :: :ok
+  @spec copy(module()) :: :ok
   def copy(module) do
     if not Code.ensure_compiled?(module) do
       raise ArgumentError,
@@ -189,49 +311,58 @@ defmodule Mimic do
   If you want to verify expectations for all tests, you can use
   `verify_on_exit!/1` as a setup callback:
 
-      setup :verify_on_exit!
-
+  ```elixir
+  setup :verify_on_exit!
+  ```
   """
+  @spec verify_on_exit!(map()) :: :ok | no_return()
   def verify_on_exit!(_context \\ %{}) do
     pid = self()
 
     Server.verify_on_exit(pid)
 
-    ExUnit.Callbacks.on_exit(Mimic, fn ->
+    Callbacks.on_exit(Mimic, fn ->
       verify!(pid)
       Server.exit(pid)
     end)
   end
 
   @doc """
-  Sets the mode to private. Mocks can be set and user by the process
-
-      setup :set_mimic_private
+  Sets the mode to private. Mocks can be set and used by the process
 
+  ```elixir
+  setup :set_mimic_private
+  ```
   """
+  @spec set_mimic_private(map()) :: :ok
   def set_mimic_private(_context \\ %{}), do: Server.set_private_mode()
 
   @doc """
   Sets the mode to global. Mocks can be set and used by all processes
 
-      setup :set_mimic_global
-
+  ```elixir
+  setup :set_mimic_global
+  ```
   """
+  @spec set_mimic_global(map()) :: :ok
   def set_mimic_global(_context \\ %{}), do: Server.set_global_mode(self())
 
   @doc """
   Chooses the mode based on ExUnit context. If `async` is `true` then
-  the mode is private, otherwise global
-
-      setup :set_mimic_from_context
+  the mode is private, otherwise global.
 
+  ```elixir
+  setup :set_mimic_from_context
+  ```
   """
-  def set_mimic_from_context(%{async: true} = _context), do: set_mimic_private()
+  @spec set_mimic_from_context(map()) :: :ok
+  def set_mimic_from_context(%{async: true}), do: set_mimic_private()
   def set_mimic_from_context(_context), do: set_mimic_global()
 
   @doc """
   Verify if expectations were fulfilled for a process `pid`
   """
+  @spec verify!(pid()) :: :ok
   def verify!(pid \\ self()) do
     pending = Server.verify(pid)
 
@@ -250,4 +381,17 @@ defmodule Mimic do
 
     :ok
   end
+
+  defp raise_if_not_copied!(module) do
+    unless function_exported?(module, :__mimic_info__, 0) do
+      raise ArgumentError,
+            "Module #{inspect(module)} has not been copied.  See docs for Mimic.copy/1"
+    end
+  end
+
+  defp raise_if_not_exported_function!(module, fn_name, arity) do
+    unless function_exported?(module, fn_name, arity) do
+      raise ArgumentError, "Function #{fn_name}/#{arity} not defined for #{inspect(module)}"
+    end
+  end
 end
diff --git a/lib/mimic/application.ex b/lib/mimic/application.ex
new file mode 100644
index 0000000..7408572
--- /dev/null
+++ b/lib/mimic/application.ex
@@ -0,0 +1,10 @@
+defmodule Mimic.Application do
+  use Application
+  alias Mimic.Server
+  @moduledoc false
+
+  def start(_, _) do
+    children = [Server]
+    Supervisor.start_link(children, name: Mimic.Supervisor, strategy: :one_for_one)
+  end
+end
diff --git a/lib/mimic/error.ex b/lib/mimic/error.ex
new file mode 100644
index 0000000..0a615e6
--- /dev/null
+++ b/lib/mimic/error.ex
@@ -0,0 +1,9 @@
+defmodule Mimic.Error do
+  defexception ~w(module fn_name arity)a
+  @moduledoc false
+
+  def message(e) do
+    mfa = Exception.format_mfa(e.module, e.fn_name, e.arity)
+    "#{mfa} cannot be stubbed as original module does not export such function"
+  end
+end
diff --git a/lib/mimic/module.ex b/lib/mimic/module.ex
index 0e17dcb..7b482fd 100644
--- a/lib/mimic/module.ex
+++ b/lib/mimic/module.ex
@@ -1,4 +1,5 @@
 defmodule Mimic.Module do
+  alias Mimic.Server
   @moduledoc false
 
   def original(module), do: "#{module}.Mimic.Original.Module" |> String.to_atom()
@@ -31,6 +32,9 @@ defmodule Mimic.Module do
         {:ok, {_, [{:abstract_code, {:raw_abstract_v1, forms}}]}} -> forms
         {:ok, {_, [{:abstract_code, :no_abstract_code}]}} -> throw(:no_abstract_code)
       end
+
+    result =
+      result
       |> rename_attribute(new_module)
 
     case :compile.forms(result, [:return_errors]) do
@@ -38,7 +42,7 @@ defmodule Mimic.Module do
         load_binary(module_name, binary)
         binary
 
-      {:ok, module_name, binary, _Warnings} ->
+      {:ok, module_name, binary, _warnings} ->
         load_binary(module_name, binary)
         Binary
 
@@ -72,7 +76,7 @@ defmodule Mimic.Module do
     module
   end
 
-  defp module_mimic_info() do
+  defp module_mimic_info do
     quote do: def(__mimic_info__, do: :ok)
   end
 
@@ -81,11 +85,15 @@ defmodule Mimic.Module do
 
     for {fn_name, arity} <- module.module_info(:exports),
         {fn_name, arity} not in internal_functions do
-      args = 0..arity |> Enum.to_list() |> tl() |> Enum.map(&Macro.var(:"arg_#{&1}", Elixir))
+      args =
+        0..arity
+        |> Enum.to_list()
+        |> tl()
+        |> Enum.map(&Macro.var(String.to_atom("arg_#{&1}"), Elixir))
 
       quote do
         def unquote(fn_name)(unquote_splicing(args)) do
-          Mimic.Server.apply(__MODULE__, unquote(fn_name), unquote(args))
+          Server.apply(__MODULE__, unquote(fn_name), unquote(args))
         end
       end
     end
diff --git a/lib/mimic/server.ex b/lib/mimic/server.ex
index e2ef054..2bfbd39 100644
--- a/lib/mimic/server.ex
+++ b/lib/mimic/server.ex
@@ -315,7 +315,11 @@ defmodule Mimic.Server do
   end
 
   defp stub_function(module, fn_name, arity) do
-    args = 0..arity |> Enum.to_list() |> tl |> Enum.map(fn i -> Macro.var(:"arg_#{i}", nil) end)
+    args =
+      0..arity
+      |> Enum.to_list()
+      |> tl
+      |> Enum.map(fn i -> Macro.var(String.to_atom("arg_#{i}"), nil) end)
 
     clause =
       quote do
diff --git a/lib/mimic/unexpected_call_error.ex b/lib/mimic/unexpected_call_error.ex
new file mode 100644
index 0000000..ea3c90f
--- /dev/null
+++ b/lib/mimic/unexpected_call_error.ex
@@ -0,0 +1,4 @@
+defmodule Mimic.UnexpectedCallError do
+  defexception [:message]
+  @moduledoc false
+end
diff --git a/lib/mimic/verification_error.ex b/lib/mimic/verification_error.ex
new file mode 100644
index 0000000..1773ed1
--- /dev/null
+++ b/lib/mimic/verification_error.ex
@@ -0,0 +1,4 @@
+defmodule Mimic.VerificationError do
+  defexception [:message]
+  @moduledoc false
+end
diff --git a/mix.exs b/mix.exs
index 281af9b..acf4b00 100644
--- a/mix.exs
+++ b/mix.exs
@@ -20,7 +20,7 @@ defmodule Mimic.Mixfile do
   def application do
     [
       extra_applications: [:logger],
-      mod: {Mimic, []}
+      mod: {Mimic.Application, []}
     ]
   end
 
@@ -28,7 +28,10 @@ defmodule Mimic.Mixfile do
   defp elixirc_paths(_), do: ["lib"]
 
   defp deps do
-    [{:ex_doc, "== 0.18.4", only: :dev}]
+    [
+      {:ex_doc, "~> 0.19", only: :dev},
+      {:credo, "~> 0.10.0", only: :dev}
+    ]
   end
 
   defp package do
diff --git a/mix.lock b/mix.lock
index ccd7f8c..5ee4c0b 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,4 +1,10 @@
 %{
+  "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
+  "credo": {:hex, :credo, "0.10.1", "e85efaf2dd7054399083ab2c6a5199f6cb1805de1a5b00d9e8c5f07033407b1f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
   "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
-  "ex_doc": {:hex, :ex_doc, "0.18.4", "4406b8891cecf1352f49975c6d554e62e4341ceb41b9338949077b0d4a97b949", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
+  "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
+  "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
+  "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
+  "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
+  "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
 }


Test patch:
diff --git a/test/mimic_test.exs b/test/mimic_test.exs
index 2830441..52a1f6e 100644
--- a/test/mimic_test.exs
+++ b/test/mimic_test.exs
@@ -149,9 +149,11 @@ defmodule Mimic.Test do
     end
 
     test "raises if a non copied module is given" do
-      assert_raise ArgumentError, "Module String not mocked", fn ->
-        stub(String, :split, fn x, y -> x + y end)
-      end
+      assert_raise ArgumentError,
+                   "Module String has not been copied.  See docs for Mimic.copy/1",
+                   fn ->
+                     stub(String, :split, fn x, y -> x + y end)
+                   end
     end
 
     test "raises if function is not in behaviour" do
@@ -248,9 +250,11 @@ defmodule Mimic.Test do
     end
 
     test "raises if a non copied module is given" do
-      assert_raise ArgumentError, "Module String not mocked", fn ->
-        stub(String, :split, fn x, y -> x + y end)
-      end
+      assert_raise ArgumentError,
+                   "Module String has not been copied.  See docs for Mimic.copy/1",
+                   fn ->
+                     stub(String, :split, fn x, y -> x + y end)
+                   end
     end
 
     test "raises if function is not defined" do
@@ -326,9 +330,11 @@ defmodule Mimic.Test do
     end
 
     test "raises if a non copied module is given" do
-      assert_raise ArgumentError, "Module String not mocked", fn ->
-        expect(String, :split, fn x, y -> x + y end)
-      end
+      assert_raise ArgumentError,
+                   "Module String has not been copied.  See docs for Mimic.copy/1",
+                   fn ->
+                     expect(String, :split, fn x, y -> x + y end)
+                   end
     end
 
     test "expecting when mock is not defined" do
@@ -433,9 +439,11 @@ defmodule Mimic.Test do
     end
 
     test "raises if a non copied module is given" do
-      assert_raise ArgumentError, "Module String not mocked", fn ->
-        expect(String, :split, fn x, y -> x + y end)
-      end
+      assert_raise ArgumentError,
+                   "Module String has not been copied.  See docs for Mimic.copy/1",
+                   fn ->
+                     expect(String, :split, fn x, y -> x + y end)
+                   end
     end
 
     test "expecting when mock is not defined" do
@@ -469,9 +477,11 @@ defmodule Mimic.Test do
     end
 
     test "raises if a non copied module is given" do
-      assert_raise ArgumentError, "Module String not mocked", fn ->
-        expect(String, :split, fn x, y -> x + y end)
-      end
+      assert_raise ArgumentError,
+                   "Module String has not been copied.  See docs for Mimic.copy/1",
+                   fn ->
+                     expect(String, :split, fn x, y -> x + y end)
+                   end
     end
 
     test "expecting when mock is not defined" do
diff --git a/test/support/test_modules.ex b/test/support/test_modules.ex
index 0b3cb8c..dc6215b 100644
--- a/test/support/test_modules.ex
+++ b/test/support/test_modules.ex
@@ -1,14 +1,17 @@
 defmodule Calculator do
+  @moduledoc false
   def add(x, y), do: x + y
   def mult(x, y), do: x * y
 end
 
 defmodule Counter do
+  @moduledoc false
   def inc(counter), do: counter + 1
   def dec(counter), do: counter - 1
   def add(counter, x), do: counter + x
 end
 
 defmodule Enumerator do
+  @moduledoc false
   def to_list(x, y), do: Enum.to_list(x..y)
 end