Repository: msz/hammox
Pull request #71 (2021-01-23 18:21:20)

add Hammox.ProtectCase


Patch:
diff --git a/lib/hammox.ex b/lib/hammox.ex
index a62477e..ff54597 100644
--- a/lib/hammox.ex
+++ b/lib/hammox.ex
@@ -115,8 +115,8 @@ defmodule Hammox do
   def protect({module, function_name, arity} = mfa, behaviour_name)
       when is_atom(module) and is_atom(function_name) and is_integer(arity) and
              is_atom(behaviour_name) do
-    module_exist?(module)
-    module_exist?(behaviour_name)
+    Utils.check_module_exists(module)
+    Utils.check_module_exists(behaviour_name)
     mfa_exist?(mfa)
 
     code = {module, function_name}
@@ -467,18 +467,6 @@ defmodule Hammox do
     Enum.at(arg_typespecs, arg_index)
   end
 
-  defp module_exist?(module) do
-    case Code.ensure_compiled(module) do
-      {:module, _} ->
-        true
-
-      _ ->
-        raise(ArgumentError,
-          message: "Could not find module #{Utils.module_to_string(module)}."
-        )
-    end
-  end
-
   defp mfa_exist?({module, function_name, arity}) do
     case function_exported?(module, function_name, arity) do
       true ->
@@ -493,7 +481,7 @@ defmodule Hammox do
   end
 
   defp get_funcs!(module) do
-    module_exist?(module)
+    Utils.check_module_exists(module)
 
     module
     |> fetch_callbacks()
diff --git a/lib/hammox/application.ex b/lib/hammox/application.ex
index ba1beb4..963de90 100644
--- a/lib/hammox/application.ex
+++ b/lib/hammox/application.ex
@@ -1,5 +1,4 @@
 defmodule Hammox.Application do
-  @moduledoc false
   use Application
 
   def start(_, _) do
diff --git a/lib/hammox/cache.ex b/lib/hammox/cache.ex
index 27d88f9..b342b15 100644
--- a/lib/hammox/cache.ex
+++ b/lib/hammox/cache.ex
@@ -1,5 +1,4 @@
 defmodule Hammox.Cache do
-  @moduledoc false
   use Agent
 
   def start_link(_initial_value) do
diff --git a/lib/hammox/protect.ex b/lib/hammox/protect.ex
new file mode 100644
index 0000000..a1af764
--- /dev/null
+++ b/lib/hammox/protect.ex
@@ -0,0 +1,109 @@
+defmodule Hammox.Protect do
+  @moduledoc """
+  A `use`able module simplifying protecting functions with Hammox.
+
+  The explicit way is to use `Hammox.protect/3` and friends to generate
+  protected versions of functions as anonymous functions. In tests, the most
+  convenient way is to generate them once in a setup hook and then resolve
+  them from test context. However, this can get quite verbose.
+
+  If you're willing to trade explicitness for some macro magic, doing `use
+  Hammox.Protect` in your test module will define functions from the module
+  you want to protect in it. The effect is similar to `import`ing the module
+  you're testing, but with added benefit of the functions being protected.
+
+  `use Hammox.Protect` supports these options:
+  - `:module` (required) — the module you'd like to protect (usually the one
+  you're testing in the test module). Equivalent to the first parameter of
+  `Hammox.protect/3` in batch usage.
+  - `:behaviour` — the behaviour module you'd like to protect the
+  implementation module with. Can be skipped if `:module` and `:behaviour`
+  are the same module. Equivalent to the second parameter of
+  `Hammox.protect/3` in batch usage.
+  - `:funs` — An optional explicit list of functions you'd like to protect.
+  Equivalent to the third parameter of `Hammox.protect/3` in batch usage.
+  """
+  alias Hammox.Utils
+
+  defmacro __using__(opts) do
+    opts_block =
+      quote do
+        {module, behaviour, funs} = Hammox.Protect.extract_opts!(unquote(opts))
+      end
+
+    funs_block =
+      quote unquote: false do
+        for {name, arity} <- funs do
+          def unquote(name)(
+                unquote_splicing(
+                  Enum.map(
+                    case arity do
+                      0 -> []
+                      arity -> 1..arity
+                    end,
+                    &Macro.var(:"arg#{&1}", __MODULE__)
+                  )
+                )
+              ) do
+            protected_fun =
+              Hammox.Protect.protect(
+                {unquote(module), unquote(name), unquote(arity)},
+                unquote(behaviour)
+              )
+
+            apply(
+              protected_fun,
+              unquote(
+                Enum.map(
+                  case arity do
+                    0 -> []
+                    arity -> 1..arity
+                  end,
+                  &Macro.var(:"arg#{&1}", __MODULE__)
+                )
+              )
+            )
+          end
+        end
+      end
+
+    quote do
+      unquote(opts_block)
+      unquote(funs_block)
+    end
+  end
+
+  @doc false
+  def extract_opts!(opts) do
+    module = Keyword.get(opts, :module)
+    behaviour = Keyword.get(opts, :behaviour)
+
+    if is_nil(module) do
+      raise ArgumentError,
+        message: """
+        Please specify :module to protect with Hammox.Protect.
+        Example:
+
+          use Hammox.Protect, module: ModuleToProtect
+
+        """
+    end
+
+    funs = Keyword.get_lazy(opts, :funs, fn -> get_funs!(behaviour || module) end)
+
+    {module, behaviour, funs}
+  end
+
+  @doc false
+  def protect(mfa, nil), do: Hammox.protect(mfa)
+  def protect(mfa, behaviour), do: Hammox.protect(mfa, behaviour)
+
+  defp get_funs!(module) do
+    Utils.check_module_exists(module)
+    {:ok, callbacks} = Code.Typespec.fetch_callbacks(module)
+
+    Enum.map(callbacks, fn {callback, _typespecs} ->
+      callback
+    end)
+  end
+end
diff --git a/lib/hammox/utils.ex b/lib/hammox/utils.ex
index a87db99..1c0de49 100644
--- a/lib/hammox/utils.ex
+++ b/lib/hammox/utils.ex
@@ -38,4 +38,16 @@ defmodule Hammox.Utils do
         other
     end
   end
+
+  def check_module_exists(module) do
+    case Code.ensure_compiled(module) do
+      {:module, _} ->
+        true
+
+      _ ->
+        raise(ArgumentError,
+          message: "Could not find module #{module_to_string(module)}."
+        )
+    end
+  end
 end


Test patch:
diff --git a/test/hammox/protect_test.exs b/test/hammox/protect_test.exs
new file mode 100644
index 0000000..34a8b2d
--- /dev/null
+++ b/test/hammox/protect_test.exs
@@ -0,0 +1,32 @@
+defmodule Hammox.ProtectTest do
+  alias Hammox.Test.Protect, as: ProtectTest
+
+  use ExUnit.Case, async: true
+
+  use Hammox.Protect, module: ProtectTest.BehaviourImplementation
+
+  use Hammox.Protect,
+    module: ProtectTest.Implementation,
+    behaviour: ProtectTest.Behaviour,
+    funs: [behaviour_wrong_typespec: 0]
+
+  test "using Protect without module throws an exception" do
+    module_string = """
+    defmodule ProtectWithoutModule do
+      use Hammox.Protect
+    end
+    """
+
+    assert_raise ArgumentError, ~r/Please specify :module to protect/, fn ->
+      Code.compile_string(module_string)
+    end
+  end
+
+  test "using Protect creates protected versions of functions from given behaviour-implementation module" do
+    assert_raise Hammox.TypeMatchError, fn -> behaviour_implementation_wrong_typespec() end
+  end
+
+  test "using Protect creates protected versions of functions from given behaviour and implementation" do
+    assert_raise Hammox.TypeMatchError, fn -> behaviour_wrong_typespec() end
+  end
+end
diff --git a/test/support/protect/behaviour.ex b/test/support/protect/behaviour.ex
new file mode 100644
index 0000000..010dda5
--- /dev/null
+++ b/test/support/protect/behaviour.ex
@@ -0,0 +1,3 @@
+defmodule Hammox.Test.Protect.Behaviour do
+  @callback behaviour_wrong_typespec() :: :foo
+end
diff --git a/test/support/protect/behaviour_implementation.ex b/test/support/protect/behaviour_implementation.ex
new file mode 100644
index 0000000..c353dfd
--- /dev/null
+++ b/test/support/protect/behaviour_implementation.ex
@@ -0,0 +1,6 @@
+defmodule Hammox.Test.Protect.BehaviourImplementation do
+  @callback behaviour_implementation_wrong_typespec() :: :foo
+  def behaviour_implementation_wrong_typespec() do
+    :wrong
+  end
+end
diff --git a/test/support/protect/implementation.ex b/test/support/protect/implementation.ex
new file mode 100644
index 0000000..38ed29d
--- /dev/null
+++ b/test/support/protect/implementation.ex
@@ -0,0 +1,3 @@
+defmodule Hammox.Test.Protect.Implementation do
+  def behaviour_wrong_typespec, do: :wrong
+end