Repository: edgurgel/mimic
Pull request #22 (2020-12-10 02:39:08)

Created DSL where stubs and expectations can be set
Created DSL where stubs and expectations can be set with a syntax that looks like a normal function call.
```elixir
test "invokes add once and mult twice" do
  Calculator
  |> stub(:add, fn x, y -> :stub end)
  |> expect(:add, fn x, y -> x + y end)
  |> expect(:mult, 2, fn x, y -> x * y end)

  assert Calculator.add(2, 3) == 5
  assert Calculator.mult(2, 3) == 6

  assert Calculator.add(2, 3) == :stub
end
```
could now be expressed as
```elixir
  test "basic example" do
    stub Calculator.add(_x, _y), do: :stub
    expect Calculator.add(x, y), do: x + y
    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) == :stub
  end
```
Hope you like it.

edgurgel: Thanks for submitting this change 🎉 

I really like that this is optional! Should we also add a section to the README.md so people can see that this DSL is available?


calvin-kargo: Cool PR !! definitely is going to try this out when it's released!

A bit wondering though, why on `stub` on DSL mode is called `allow` ?
bbalser: Changed `allow` to `stub`, because I wasn't thinking clearly when I wrote that ;)

Added `num_calls` option to expect calls

Added section to README
edgurgel: This is great! thank you!

Patch:
diff --git a/.formatter.exs b/.formatter.exs
index 8632f81..e08fba8 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -3,5 +3,9 @@
     "lib/**/*.{ex,exs}",
     "test/**/*.{ex,exs}",
     "mix.exs"
+  ],
+  locals_without_parens: [allow: :*, expect: :*],
+  export: [
+    locals_without_parens: [allow: :*, expect: :*]
   ]
 ]
diff --git a/README.md b/README.md
index f8e36e3..fec56d4 100644
--- a/README.md
+++ b/README.md
@@ -155,6 +155,25 @@ defined with expect & stub. This option is simpler but tests running
 concurrently will have undefined behaviour. It is important to run with `async: false`.
 One could use `:set_mimic_from_context` instead of using `:set_mimic_global` or `:set_mimic_private`. It will be private if `async: true`, global otherwise.
 
+## DSL Mode
+To use DSL Mode `use Mimic.DSL` rather than `use Mimic` in your test.  DSL Mode enables a more expressive api to the Mimic functionality.
+
+```elixir
+  use Mimic.DSL
+
+  test "basic example" do
+    stub Calculator.add(_x, _y), do: :stub
+    expect Calculator.add(x, y), do: x + y
+    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) == :stub
+  end
+```
+
+
 ## Implementation Details & Performance
 
 After calling `Mimic.copy(MyModule)`, calls to functions belonging to this module will first go through an ETS table to check which pid sees what (stubs, expects or call original).
diff --git a/lib/mimic/dsl.ex b/lib/mimic/dsl.ex
new file mode 100644
index 0000000..1c0cc29
--- /dev/null
+++ b/lib/mimic/dsl.ex
@@ -0,0 +1,111 @@
+defmodule Mimic.DSL do
+  @moduledoc """
+  stubs and expectations can be expressed in a more natural way
+
+  ```elixir
+  use Mimic.DSL
+  ```
+
+  ```elixir
+  test "basic example" do
+    stub Calculator.add(_x, _y), do: :stub
+    expect Calculator.add(x, y), do: x + y
+    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) == :stub
+  end
+  ```
+
+  Support for expecting multiple calls
+  ```elixir
+  expect Calculator.add(x, y), num_calls: 2 do
+    x + y
+  end
+  ```
+
+  """
+
+  @doc false
+  defmacro __using__(_opts) do
+    quote do
+      import Mimic, except: [stub: 3, expect: 3, expect: 4]
+      import Mimic.DSL
+      setup :verify_on_exit!
+    end
+  end
+
+  defmacro stub({{:., _, [module, f]}, _, args}, opts) do
+    body = Keyword.fetch!(opts, :do)
+
+    function =
+      quote do
+        fn unquote_splicing(args) ->
+          unquote(body)
+        end
+      end
+
+    quote do
+      Mimic.stub(unquote(module), unquote(f), unquote(function))
+    end
+  end
+
+  defmacro stub({:when, _, [{{:., _, [module, f]}, _, args}, guard_args]}, opts) do
+    body = Keyword.fetch!(opts, :do)
+
+    function =
+      quote do
+        fn unquote_splicing(args) when unquote(guard_args) ->
+          unquote(body)
+        end
+      end
+
+    quote do
+      Mimic.stub(unquote(module), unquote(f), unquote(function))
+    end
+  end
+
+  defmacro expect(ast, opts \\ [], do_block)
+
+  defmacro expect({{:., _, [module, f]}, _, args}, opts, do_opts) do
+    num_calls =
+      Keyword.get_lazy(opts, :num_calls, fn ->
+        Keyword.get(do_opts, :num_calls, 1)
+      end)
+
+    body = Keyword.fetch!(do_opts, :do)
+
+    function =
+      quote do
+        fn unquote_splicing(args) ->
+          unquote(body)
+        end
+      end
+
+    quote do
+      Mimic.expect(unquote(module), unquote(f), unquote(num_calls), unquote(function))
+    end
+  end
+
+  defmacro expect({:when, _, [{{:., _, [module, f]}, _, args}, guard_args]}, opts, do_opts) do
+    num_calls =
+      Keyword.get_lazy(opts, :num_calls, fn ->
+        Keyword.get(do_opts, :num_calls, 1)
+      end)
+
+    body = Keyword.fetch!(do_opts, :do)
+
+    function =
+      quote do
+        fn unquote_splicing(args) when unquote(guard_args) ->
+          unquote(body)
+        end
+      end
+
+    quote do
+      Mimic.expect(unquote(module), unquote(f), unquote(num_calls), unquote(function))
+    end
+  end
+end


Test patch:
diff --git a/test/dsl_test.exs b/test/dsl_test.exs
new file mode 100644
index 0000000..fa63516
--- /dev/null
+++ b/test/dsl_test.exs
@@ -0,0 +1,60 @@
+defmodule Mimic.DSLTest do
+  use ExUnit.Case, async: true
+  use Mimic.DSL
+
+  test "basic example" do
+    stub(Calculator.add(_x, _y), do: :stub)
+    expect Calculator.add(x, y), do: x + y
+    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) == :stub
+  end
+
+  test "guards on stub" do
+    stub Calculator.add(x, y) when rem(x, 2) == 0 and y == 2 do
+      x + y
+    end
+
+    assert Calculator.add(2, 2) == 4
+
+    assert_raise FunctionClauseError, fn ->
+      Calculator.add(3, 1)
+    end
+  end
+
+  test "guards on expect" do
+    expect Calculator.add(x, y) when rem(x, 2) == 0 and y == 2 do
+      x + y
+    end
+
+    assert_raise FunctionClauseError, fn ->
+      Calculator.add(3, 1)
+    end
+  end
+
+  test "expect supports optional num_calls" do
+    n = 2
+
+    expect Calculator.add(x, y), num_calls: n do
+      x + y
+    end
+
+    assert Calculator.add(1, 3) == 4
+    assert Calculator.add(1, 4) == 5
+  end
+
+  test "expect supports optional num_calls with guard clause" do
+    expect Calculator.add(x, y) when x == 1, num_calls: 2 do
+      x + y
+    end
+
+    assert Calculator.add(1, 3) == 4
+
+    assert_raise FunctionClauseError, fn ->
+      Calculator.add(2, 4) == 6
+    end
+  end
+end