Repository: edgurgel/mimic
Pull request #98 (2025-06-03 10:05:43)

feat!: change expect/3,4 to not call original once fulfilled
It closes https://github.com/edgurgel/mimic/issues/60

TODO:

* [x] Update README outlining breaking change from Mimic 1 to 2.
* [x] Update `expect/3,4` docs


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..9724d15
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,57 @@
+# 2.0.0 (2025-07-13)
+
+## Breaking changes
+
+The code below would call the original `Calculator.add/2` when all expectations were fulfilled.
+
+```elixir
+ Calculator
+ |> expect(:add, fn _, _ -> :expected1 end)
+ |> expect(:add, fn _, _ -> :expected2 end)
+
+ assert Calculator.add(1, 1) == :expected1
+ assert Calculator.add(1, 1) == :expected2
+ assert Calculator.add(1, 1) == 2
+```
+
+Now with Mimic 2 this will raise:
+
+```elixir
+ Calculator
+ |> expect(:add, fn _, _ -> :expected1 end)
+ |> expect(:add, fn _, _ -> :expected2 end)
+
+ assert Calculator.add(1, 1) == :expected1
+ assert Calculator.add(1, 1) == :expected2
+ Calculator.add(1, 1)
+# Will raise error because more than 2 calls to Calculator.add were made and there is no stub
+# ** (Mimic.UnexpectedCallError) Calculator.add/2 called in process #PID<.*> but expectations are already fulfilled
+```
+
+If there is a stub the stub will be called instead. This behaviour is the same as before.
+
+```elixir
+ Calculator
+ |> expect(:add, fn _, _ -> :expected1 end)
+ |> expect(:add, fn _, _ -> :expected2 end)
+ |> stub(:add, fn _, _ -> :stub end)
+
+ assert Calculator.add(1, 1) == :expected1
+ assert Calculator.add(1, 1) == :expected2
+ assert Calculator.add(1, 1) == :stub
+```
+
+Which means that if someone wants to keep the original behaviour on Mimic 1.* just do the following:
+
+```elixir
+ Calculator
+ |> expect(:add, fn _, _ -> :expected1 end)
+ |> expect(:add, fn _, _ -> :expected2 end)
+ |> stub(:add, fn x, y -> call_original(Calculator, :add, [x, y]) end)
+
+ assert Calculator.add(1, 1) == :expected1
+ assert Calculator.add(1, 1) == :expected2
+ assert Calculator.add(1, 1) == 2
+```
+
+This way once all expectations are fulfilled the original function is called again.
diff --git a/README.md b/README.md
index 2992c39..a16f146 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Just add `:mimic` to your list of dependencies in `mix.exs`:
 ```elixir
 def deps do
   [
-    {:mimic, "~> 1.12", only: :test}
+    {:mimic, "~> 2.0", only: :test}
   ]
 end
 ```
@@ -101,6 +101,10 @@ Calculator
 
 assert Calculator.add(1, 3) == {:add, 1, 3}
 assert Calculator.add(4, 5) == {:add, 4, 5}
+Calculator.add(1, 4)
+
+# Will raise error because more than 2 calls to Calculator.add were made and there is no stub
+# ** (Mimic.UnexpectedCallError) Calculator.add/2 called in process #PID<.*> but expectations are already fulfilled
 ```
 
 With `use Mimic`, verification `expect/4` function call of is done automatically on test case end. `verify!/1` can be used in case custom verification timing required:
diff --git a/lib/mimic.ex b/lib/mimic.ex
index 93413b2..e0553da 100644
--- a/lib/mimic.ex
+++ b/lib/mimic.ex
@@ -196,6 +196,9 @@ defmodule Mimic do
   function must be called within the lifetime of the calling `pid` (i.e. the
   test example).
 
+  Once the expectation is fulfilled any calls to this MFA will raise an error
+  unless there is a stub through `stub/3`
+
   ## Arguments:
 
     * `module` - the name of the module in which we're adding the stub.
diff --git a/lib/mimic/server.ex b/lib/mimic/server.ex
index 75609d9..59f503f 100644
--- a/lib/mimic/server.ex
+++ b/lib/mimic/server.ex
@@ -139,6 +139,12 @@ defmodule Mimic.Server do
       :original ->
         apply_original(module, fn_name, args)
 
+      {:unexpected, :fulfilled} ->
+        mfa = Exception.format_mfa(module, fn_name, arity)
+
+        raise Mimic.UnexpectedCallError,
+              "#{mfa} called in process #{inspect(self())} but expectations are already fulfilled"
+
       {:unexpected, num_calls, num_applied_calls} ->
         mfa = Exception.format_mfa(module, fn_name, arity)
 
@@ -283,16 +289,22 @@ defmodule Mimic.Server do
             {:reply, {:unexpected, num_calls, num_applied_calls}, state}
         end
 
-      _ ->
-        case find_stub(state.stubs, module, fn_name, arity, caller) do
-          :unexpected ->
-            {:reply, :original, state}
-
-          {:ok, func} ->
+      expectations ->
+        case {find_stub(state.stubs, module, fn_name, arity, caller), expectations} do
+          {{:ok, func}, _} ->
             # Track call history for stubs too
             state = put_call_history(state, caller, module, fn_name, arity, args)
 
             {:reply, {:ok, func}, state}
+
+          {:unexpected, []} ->
+            # expectations for this mfa existed but they have all been fulfilled
+            {:reply, {:unexpected, :fulfilled}, state}
+
+          {:unexpected, nil} ->
+            # no expectations were ever defined for this mfa
+            # This case happens when another mfa was set-up
+            {:reply, :original, state}
         end
     end
   end
diff --git a/mix.exs b/mix.exs
index de027b8..b04367b 100644
--- a/mix.exs
+++ b/mix.exs
@@ -2,7 +2,7 @@ defmodule Mimic.Mixfile do
   use Mix.Project
 
   @source_url "https://github.com/edgurgel/mimic"
-  @version "1.12.0"
+  @version "2.0.0"
 
   def project do
     [


Test patch:
diff --git a/test/dsl_test.exs b/test/dsl_test.exs
index e985aa3..1bbe6de 100644
--- a/test/dsl_test.exs
+++ b/test/dsl_test.exs
@@ -9,12 +9,14 @@ defmodule Mimic.DSLTest do
   test "basic example" do
     stub(Calculator.add(_x, _y), do: @stub)
     expect Calculator.add(x, y), do: x + y
+    expect Calculator.add(x, y), do: x + y + 1
     expect Calculator.mult(x, y), do: x * y
 
     assert Calculator.add(2, 3) == 5
     assert Calculator.mult(2, 3) == 6
+    assert Calculator.add(2, 3) == 6
 
-    assert Calculator.add(2, 3) == @stub
+    assert Calculator.add(5, 3) == @stub
   end
 
   test "guards on stub" do
diff --git a/test/mimic_test.exs b/test/mimic_test.exs
index aec1f27..a225ec6 100644
--- a/test/mimic_test.exs
+++ b/test/mimic_test.exs
@@ -214,7 +214,7 @@ defmodule Mimic.Test do
       assert Calculator.add(3, 4) == 7
     end
 
-    test "invokes stub after expectations are fulfilled" do
+    test "return stub when all expectations are fulfilled and another call is made" do
       Calculator
       |> stub(:add, fn _x, _y -> @stubbed end)
       |> expect(:add, fn _, _ -> @expected_1 end)
@@ -223,6 +223,7 @@ defmodule Mimic.Test do
       assert Calculator.add(1, 1) == @expected_1
       assert Calculator.add(1, 1) == @expected_2
       assert Calculator.add(1, 1) == @stubbed
+
       verify!()
     end
 
@@ -310,7 +311,7 @@ defmodule Mimic.Test do
       assert_receive :ok
     end
 
-    test "invokes stub after expectations are fulfilled" do
+    test "raise when all expectations are fulfilled and another call is made" do
       Calculator
       |> stub(:add, fn _x, _y -> @stubbed end)
       |> expect(:add, fn _, _ -> @expected end)
@@ -431,7 +432,21 @@ defmodule Mimic.Test do
       verify!(self())
     end
 
-    test "expecting when no expectation is defined calls original" do
+    test "raise when all expectations are fulfilled and another call is made" do
+      Calculator
+      |> expect(:add, fn x, _y -> x + 2 end)
+      |> expect(:mult, fn x, _y -> x * 2 end)
+
+      assert Calculator.add(4, 0) == 4 + 2
+      assert Calculator.mult(5, 0) == 5 * 2
+
+      message =
+        ~r"Calculator.mult/2 called in process #PID<.*> but expectations are already fulfilled"
+
+      assert_raise Mimic.UnexpectedCallError, message, fn -> Calculator.mult(5, 3) end
+    end
+
+    test "expectation can be added after being fulfilled" do
       Calculator
       |> expect(:add, fn x, _y -> x + 2 end)
       |> expect(:mult, fn x, _y -> x * 2 end)
@@ -439,7 +454,20 @@ defmodule Mimic.Test do
       assert Calculator.add(4, 0) == 4 + 2
       assert Calculator.mult(5, 0) == 5 * 2
 
-      assert Calculator.mult(5, 3) == 15
+      message =
+        ~r"Calculator.mult/2 called in process #PID<.*> but expectations are already fulfilled"
+
+      assert_raise Mimic.UnexpectedCallError, message, fn -> Calculator.mult(5, 3) end
+
+      Calculator
+      |> expect(:mult, fn x, _y -> x * 2 end)
+
+      assert Calculator.mult(4, 0) == 4 * 2
+
+      message =
+        ~r"Calculator.mult/2 called in process #PID<.*> but expectations are already fulfilled"
+
+      assert_raise Mimic.UnexpectedCallError, message, fn -> Calculator.mult(5, 3) end
     end
 
     test "raises if a non copied module is given" do
@@ -483,6 +511,14 @@ defmodule Mimic.Test do
       assert_receive :ok
     end
 
+    test "expectation on a function and call others" do
+      Calculator
+      |> expect(:add, fn x, _y -> x + 2 end)
+
+      assert Calculator.add(4, :_) == 6
+      assert Calculator.mult(5, 2) == 10
+    end
+
     test "stacking expectations" do
       Calculator
       |> expect(:add, fn _x, _y -> @expected_1 end)
@@ -545,7 +581,7 @@ defmodule Mimic.Test do
       verify!(self())
     end
 
-    test "expecting when no expectation is defined calls original" do
+    test "raise when all expectations are fulfilled and another call is made" do
       Calculator
       |> expect(:add, fn x, _y -> x + 2 end)
       |> expect(:mult, fn x, _y -> x * 2 end)
@@ -556,7 +592,10 @@ defmodule Mimic.Test do
         assert Calculator.add(4, :_) == 6
         assert Calculator.mult(5, :_) == 10
 
-        assert Calculator.mult(5, 3) == 15
+        message =
+          ~r"Calculator.mult/2 called in process #PID<.*> but expectations are already fulfilled"
+
+        assert_raise Mimic.UnexpectedCallError, message, fn -> Calculator.mult(5, 3) end
 
         send(parent_pid, :ok)
       end)