Repository: msz/hammox
Pull request #58 (2020-10-09 18:09:27)

Type-check stubs
Part of #56

Currently, only mocks created with `expect` are type-checked by Hammox.

This provides the same protection for stubs created with `stub`.

There is also `stub_with`, but I'm not sure how to add protection for that one; suggestions welcome!

I wasn't sure how best to test this change. For now, I modified `assert_pass` and `assert_fail` to use `stub` instead of `expect`; there are a few other tests that continue to use `expect`, so both code paths are being tested.  I'm happy to change this up however you'd like.

I extracted a helper named `wrap` that is now used in both `expect` and `stub`; I'd rather use the name `protected` for this, but that name's already taken. Again, I'm happy to change this however you'd like.

msz: Thanks! I think the code looks great.

For testing, I'd go a simpler way — I'd just make a test where we stub with a wrong function and try to call the stub, then assert that there was a Hammox exception thrown. This should ensure that stubs are being protected.
randycoulman: @msz Great, thanks! I'll take care of that tomorrow.

If you have a minute to drop a `hacktoberfest-accepted` label on this PR, I'd appreciate it.  Thanks!

Patch:
diff --git a/lib/hammox.ex b/lib/hammox.ex
index c5cd51c..4b70648 100644
--- a/lib/hammox.ex
+++ b/lib/hammox.ex
@@ -34,20 +34,18 @@ defmodule Hammox do
   See [Mox.expect/4](https://hexdocs.pm/mox/Mox.html#expect/4).
   """
   def expect(mock, name, n \\ 1, code) do
-    arity = :erlang.fun_info(code)[:arity]
-
-    hammox_code =
-      case fetch_typespecs_for_mock(mock, name, arity) do
-        # This is really an error case where we're trying to mock a function
-        # that does not exist in the behaviour. Mox will flag it better though
-        # so just let it pass through.
-        [] -> code
-        typespecs -> protected(code, typespecs, arity)
-      end
-
+    hammox_code = wrap(mock, name, code)
     Mox.expect(mock, name, n, hammox_code)
   end
 
+  @doc """
+  See [Mox.stub/3](https://hexdocs.pm/mox/Mox.html#stub/3).
+  """
+  def stub(mock, name, code) do
+    hammox_code = wrap(mock, name, code)
+    Mox.stub(mock, name, hammox_code)
+  end
+
   @doc """
   See [Mox.set_mox_from_context/1](https://hexdocs.pm/mox/Mox.html#set_mox_from_context/1).
   """
@@ -63,11 +61,6 @@ defmodule Hammox do
   """
   defdelegate set_mox_private(context \\ %{}), to: Mox
 
-  @doc """
-  See [Mox.stub/3](https://hexdocs.pm/mox/Mox.html#stub/3).
-  """
-  defdelegate stub(mock, name, code), to: Mox
-
   @doc """
   See [Mox.stub_with/2](https://hexdocs.pm/mox/Mox.html#stub_with/2).
   """
@@ -236,6 +229,18 @@ defmodule Hammox do
     |> Enum.into(%{})
   end
 
+  defp wrap(mock, name, code) do
+    arity = :erlang.fun_info(code)[:arity]
+
+    case fetch_typespecs_for_mock(mock, name, arity) do
+      # This is really an error case where we're trying to mock a function
+      # that does not exist in the behaviour. Mox will flag it better though
+      # so just let it pass through.
+      [] -> code
+      typespecs -> protected(code, typespecs, arity)
+    end
+  end
+
   defp protected(code, typespecs, 0) do
     fn ->
       protected_code(code, typespecs, [])


Test patch:
diff --git a/test/hammox_test.exs b/test/hammox_test.exs
index eaefc39..e3e23b3 100644
--- a/test/hammox_test.exs
+++ b/test/hammox_test.exs
@@ -1227,6 +1227,20 @@ defmodule HammoxTest do
     end
   end
 
+  describe "expect/4" do
+    test "protects mocks" do
+      TestMock |> expect(:foo_none, fn -> :baz end)
+      assert_raise(Hammox.TypeMatchError, fn -> TestMock.foo_none() end)
+    end
+  end
+
+  describe "stub/3" do
+    test "protects stubs" do
+      TestMock |> stub(:foo_none, fn -> :baz end)
+      assert_raise(Hammox.TypeMatchError, fn -> TestMock.foo_none() end)
+    end
+  end
+
   defp assert_pass(function_name, value) do
     TestMock |> expect(function_name, fn -> value end)
     assert value == apply(TestMock, function_name, [])