archan937

archan937

MecksUnit: Elegantly mock module functions in (async) ExUnit tests

It is a well-know topic within the Elixir community: “To mock or not to mock? :)”

Every alchemist probably has his / her own opinion concerning this topic. José Valim and Plataformatec has published the Hex package Mox which complies with his article on mocking in Elixir.

Personally, I’m not convinced in having to change the code “in service of” testing certain modules. Why would one add abstraction to code of which its purpose isn’t supposed to be interchangeable (with mock modules for instance)?

After some Googling, I found Espec of which I thought that that’s a little bit too much. Finally, I found Mock which could have done the job. But there are two downsides:

  1. You cannot use async: true
  2. Defining the mock functions could have been done in a more readable way

Based on that, I decided to write MecksUnit which solves just that. An example:

defmodule Foo do
  def trim(string) do
    String.trim(string)
  end
end

defmodule MecksUnitTest do
  use ExUnit.Case, async: true
  use MecksUnit.Case

  defmock String do
    def trim("  Paul  "), do: "Engel"
    def trim("  Foo  ", "!"), do: "Bar"
    def trim(_, "!"), do: {:passthrough, ["  Surprise!  !!!!", "!"]}
    def trim(_, _), do: :passthrough
  end

  defmock List do
    def wrap(:foo), do: [1, 2, 3, 4]
  end

  mocked_test "using mocked module functions" do
    task =
      Task.async(fn ->
        assert "Engel" == String.trim("  Paul  ")
        assert "Engel" == Foo.trim("  Paul  ")
        assert "Bar" == String.trim("  Foo  ", "!")
        assert "  Surprise!  " == String.trim("  Paul  ", "!")
        assert "MecksUnit" == String.trim("  MecksUnit  ")
        assert "Paul Engel" == String.trim("  Paul Engel  ", " ")
        assert [1, 2, 3, 4] == List.wrap(:foo)
        assert [] == List.wrap(nil)
        assert [:bar] == List.wrap(:bar)
        assert [:foo, :bar] == List.wrap([:foo, :bar])
      end)

    Task.await(task)
  end

  test "using the original module functions" do
    task =
      Task.async(fn ->
        assert "Paul" == String.trim("  Paul  ")
        assert "Paul" == Foo.trim("  Paul  ")
        assert "  Foo  " == String.trim("  Foo  ", "!")
        assert "  Paul  " == String.trim("  Paul  ", "!")
        assert "MecksUnit" == String.trim("  MecksUnit  ")
        assert "Paul Engel" == String.trim("  Paul Engel  ", " ")
        assert [:foo] == List.wrap(:foo)
        assert [] == List.wrap(nil)
        assert [:bar] == List.wrap(:bar)
        assert [:foo, :bar] == List.wrap([:foo, :bar])
      end)

    Task.await(task)
  end

  defmock String do
    def trim("  Paul  "), do: "PAUL :)"
  end

  defmock List do
    def wrap([1, 2, 3, 4]), do: [5, 6, 7, 8]
    def wrap(nil), do: ~w(Surprise)
  end

  mocked_test "using different mocked module functions" do
    task =
      Task.async(fn ->
        assert "PAUL :)" == String.trim("  Paul  ")
        assert "PAUL :)" == Foo.trim("  Paul  ")
        assert "  Foo  " == String.trim("  Foo  ", "!")
        assert "  Paul  " == String.trim("  Paul  ", "!")
        assert "MecksUnit" == String.trim("  MecksUnit  ")
        assert "Paul Engel" == String.trim("  Paul Engel  ", " ")
        assert [:foo] == List.wrap(:foo)
        assert ["Surprise"] == List.wrap(nil)
        assert [:bar] == List.wrap(:bar)
        assert [:foo, :bar] == List.wrap([:foo, :bar])
        assert [5, 6, 7, 8] == List.wrap([1, 2, 3, 4])
      end)

    Task.await(task)
  end
end

Mocking module functions is pretty straightforward and done as follows:

  1. Add use MecksUnit.Case at the beginning of your test file
  2. Use defmock as if you would define the original module with defmodule containing mocked functions
  3. Use mocked_test as if you would define a normal ExUnit test after having defined all the required mock modules

The defined mock modules only apply to the first mocked_test encountered. So they are isolated (despite of :meck having an unfortunate global effect ) as MecksUnit takes care of it. Also, non-matching function heads within the mock module will result in invoking the original module function as well. And last but not least: you can just run the tests asynchronously .

Enjoy using MecksUnit (if you prefer unobtrusive mocking). A Github star is very welcome, haha :wink:

First Post!

archan937

archan937

Released MecksUnit v0.1.2 in which you can assert function calls with either called (returns a boolean) or assert_called (raises an error when not having found a match):

assert called List.wrap(:foo)
assert_called String.trim(_)

Most Liked

archan937

archan937

Just released MecksUnit v0.1.4 :slight_smile:

archan937

archan937

MecksUnit v0.1.3 is out. It includes the fix for :meck related compile errors which often occurred when mocking within multiple files

sztosz

sztosz

I don’t know what happened but since 1.3 mocked_test fail with same defmock do block. Instead of mocking it’s passing through function.
Nevermind, I forgot to do MecksUnit.mock() Working like a charm :+1:

Last Post!

archan937

archan937

Hey @hrubi (and @sztosz), the bugfix has been released in MecksUnit v0.1.9.

Really sorry for the late reply :pray: :v:

Where Next?

Popular in Announcing Top

Crowdhailer
Experimenting with this code. OK.try do user <- fetch_user(1) cart <- fetch_cart(1) order = checkout(cart, user) save_orde...
New
bryanjos
Hi, I wanted share a small library we at Revelry Labs made for rendering react components from the server side. There are instructions fo...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44778 311
New
tmbb
PhoenixWS - Websockets over Phoenix Channels Source code on Github here: GitHub - tmbb/phoenix_ws: Websockets implemented over Phoenix Ch...
New
fuelen
Hey folks! Want to present a toolkit for writing command-line user interfaces. It provides a convenient interface for colorizing text...
New
tfwright
After working on it for a couple of months and using it in production for most of that time, today I’ve released LiveAdmin, a LiveView ba...
New
zachdaniel
Ash Framework What is Ash? Ash Framework is a declarative, resource-oriented application development framework for Elixir. A resource can...
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement