GithubHelp home page GithubHelp logo

mock's Introduction

Build Status

Mock

A mocking library for the Elixir language.

We use the Erlang meck library to provide module mocking functionality for Elixir. It uses macros in Elixir to expose the functionality in a convenient manner for integrating in Elixir tests.

See the full reference documentation.

Table of Contents

Installation

First, add mock to your mix.exs dependencies:

def deps do
  [{:mock, "~> 0.3.0", only: :test}]
end

and run $ mix deps.get.

with_mock - Mocking a single module

The Mock library provides the with_mock macro for running tests with mocks.

For a simple example, if you wanted to test some code which calls HTTPotion.get to get a webpage but without actually fetching the webpage you could do something like this:

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "test_name" do
    with_mock HTTPotion, [get: fn(_url) -> "<html></html>" end] do
      assert "<html></html>" == HTTPotion.get("http://example.com")
    end
  end
end

The with_mock macro creates a mock module. The keyword list provides a set of mock implementation for functions we want to provide in the mock (in this case just get). Inside with_mock we exercise the test code and we can check that the call was made as we expected using called and providing the example of the call we expected.

with_mocks - Mocking multiple modules

You can mock up multiple modules with with_mocks.

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "multiple mocks" do
    with_mocks([
      {Map,
       [],
       [get: fn(%{}, "http://example.com") -> "<html></html>" end]},
      {String,
       [],
       [reverse: fn(x) -> 2*x end,
        length: fn(_x) -> :ok end]}
    ]) do
      assert Map.get(%{}, "http://example.com") == "<html></html>"
      assert String.reverse(3) == 6
      assert String.length(3) == :ok
    end
  end
end

The second parameter of each tuple is opts - a list of optional arguments passed to meck.

test_with_mock - with_mock helper

An additional convenience macro test_with_mock is supplied which internally delegates to with_mock. Allowing the above test to be written as follows:

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test_with_mock "test_name", HTTPotion,
    [get: fn(_url) -> "<html></html>" end] do
    HTTPotion.get("http://example.com")
    assert_called HTTPotion.get("http://example.com")
  end
end

The test_with_mock macro can also be passed a context argument allowing the sharing of information between callbacks and the test

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  setup do
    doc = "<html></html>"
    {:ok, doc: doc}
  end

  test_with_mock "test_with_mock with context", %{doc: doc}, HTTPotion, [],
    [get: fn(_url, _headers) -> doc end] do

    HTTPotion.get("http://example.com", [foo: :bar])
    assert_called HTTPotion.get("http://example.com", :_)
  end
end

setup_with_mocks - Configure all tests to have the same mocks

The setup_with_mocks mocks up multiple modules prior to every single test along while calling the provided setup block. It is simply an integration of the with_mocks macro available in this module along with the setup macro defined in elixir's ExUnit.

defmodule MyTest do
  use ExUnit.Case, async: false
  import Mock

  setup_with_mocks([
    {Map, [], [get: fn(%{}, "http://example.com") -> "<html></html>" end]}
  ]) do
    foo = "bar"
    {:ok, foo: foo}
  end

  test "setup_with_mocks" do
    assert Map.get(%{}, "http://example.com") == "<html></html>"
  end
end

The behaviour of a mocked module within the setup call can be overridden using any of the methods above in the scope of a specific test. Providing this functionality by setup_all is more difficult, and as such, setup_all_with_mocks is not currently supported.

Currently, mocking modules cannot be done asynchronously, so make sure that you are not using async: true in any module where you are testing.

Also, because of the way mock overrides the module, it must be defined in a separate file from the test file.

Mocking input dependant output

If you have a function that should return different values depending on what the input is, you can do as follows:

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "mock functions with multiple returns" do
    with_mock(Map, [
      get: fn
        (%{}, "http://example.com") -> "<html>Hello from example.com</html>"
        (%{}, "http://example.org") -> "<html>example.org says hi</html>"
      end
    ]) do
      assert Map.get(%{}, "http://example.com") == "<html>Hello from example.com</html>"
      assert Map.get(%{}, "http://example.org") == "<html>example.org says hi</html>"
    end
  end
end

Mocking functions with different arities

You can mock functions in the same module with different arity:

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "mock functions with different arity" do
    with_mock String,
      [slice: fn(string, range)      -> string end,
       slice: fn(string, range, len) -> string end]
    do
      assert String.slice("test", 1..3) == "test"
      assert String.slice("test", 1, 3) == "test"
    end
  end
end

passthrough - partial mocking of a module

By default, only the functions being mocked can be accessed from within the test. Trying to call a non-mocked function from a mocked Module will result in an error. This can be circumvented by passing the :passthrough option like so:

defmodule MyTest do
  use ExUnit.Case, async: false
  import Mock

  test_with_mock "test_name", IO, [:passthrough], [] do
    IO.puts "hello"
    assert_called IO.puts "hello"
  end
end

Assert called - assert a specific function was called

You can check whether or not your mocked module was called.

Assert called - specific value

It is possible to assert that the mocked module was called with a specific input.

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "test_name" do
    with_mock HTTPotion, [get: fn(_url) -> "<html></html>" end] do
      HTTPotion.get("http://example.com")
      assert_called HTTPotion.get("http://example.com")
    end
  end
end

Assert called - wildcard

It is also possible to assert that the mocked module was called with any value by passing the :_ wildcard.

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "test_name" do
    with_mock HTTPotion, [get: fn(_url) -> "<html></html>" end] do
      HTTPotion.get("http://example.com")
      assert_called HTTPotion.get(:_)
    end
  end
end

Assert called - pattern matching

assert_called will check argument equality using == semantics, not pattern matching. For structs, you must provide every property present on the argument as it was called or it will fail. To use pattern matching (useful when you only care about a few properties on the argument or need to perform advanced matching like regex matching), provide custom argument matcher(s) using :meck.is/1.

defmodule User do
  defstruct [:id, :name, :email]
end

defmodule Network do
  def update(%User{} = user), do: # ...
end

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "test_name" do
    with_mock Network, [update: fn(_user) -> :ok end] do
      user = %User{id: 1, name: "Jane Doe", email: "[email protected]"}
      Network.update(user)

      assert_called Network.update(
        :meck.is(fn user ->
          assert user.__struct__ = User
          assert user.id = 1

          # matcher must return true when the match succeeds
          true
        end)
      )
    end
  end
end

Assert call order

call_history will return the meck.history(Module) allowing you assert on the order of the function invocation:

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "test_name" do
    with_mock HTTPotion, [get: fn(_url) -> "<html></html>" end] do
      HTTPotion.get("http://example.com")

      assert call_history(HTTPotion) ==
        [
          {pid, {HTTPotion, :get, ["http://example.com"]}, "<html></html>"}
        ]
    end
  end
end
  
  

You can use any valid Elixir pattern matching/multiple function heads to accomplish this more succinctly, but remember that the matcher will be executed for all function calls, so be sure to include a fallback case that returns false. For mocked functions with multiple arguments, you must include a matcher/pattern for each argument.

defmodule Network.V2 do
  def update(%User{} = user, changes), do: # ...

  def update(id, changes) when is_integer(id), do: # ...

  def update(_, _), do: # ...
end

defmodule MyTest do
  use ExUnit.Case, async: false

  import Mock

  test "test_name" do
    with_mock Network.V2, [update: fn(_user, _changes) -> :ok end] do
      Network.V2.update(%User{id: 456, name: "Jane Doe"}, %{name: "John Doe"})
      Network.V2.update(123, %{name: "John Doe", email: "[email protected]"})
      Network.V2.update(nil, %{})

      # assert that `update` was called with user id 456
      assert_called Network.V2.update(
        :meck.is(fn
          %User{id: 456} -> true
          _ -> false
        end),
        :_
      )

      # assert that `update` was called with an email change
      assert_called Network.V2.update(
        :_,
        :meck.is(fn
          %{email: "[email protected]"} -> true
          _ -> false
        end)
      )
    end
  end
end

NOT SUPPORTED - Mocking internal function calls

A common issue a lot of developers run into is Mock's lack of support for mocking internal functions. Mock will behave as follows:

defmodule MyApp.IndirectMod do

  def value do
    1
  end

  def indirect_value do
    value()
  end

  def indirect_value_2 do
    MyApp.IndirectMod.value()
  end

end
defmodule MyTest do
  use ExUnit.Case, async: false
  import Mock

  test "indirect mock" do
    with_mocks([
      { MyApp.IndirectMod, [:passthrough], [value: fn -> 2 end] },
    ]) do
      # The following assert succeeds
      assert MyApp.IndirectMod.indirect_value_2() == 2
      # The following assert also succeeds
      assert MyApp.IndirectMod.indirect_value() == 1
    end
  end
end

It is important to understand that only fully qualified function calls get mocked. The reason for this is because of the way Meck is structured. Meck creates a thin wrapper module with the name of the mocked module (and passes through any calls to the original Module in case passthrough is used). The original module is renamed, but otherwise unmodified. Once the call enters the original module, the local function call jumps stay in the module.

Big thanks to @eproxus (author of Meck) who helped explain this to me. We're looking into some alternatives to help solve this, but it is something to be aware of in the meantime. The issue is being tracked in Issue 71.

In order to workaround this issue, the indirect_value can be rewritten like so:

  def indirect_value do
    __MODULE__.value()
  end

Or, like so:

  def indirect_value do
    MyApp.IndirectMod.value()
  end

Tips

The use of mocking can be somewhat controversial. I personally think that it works well for certain types of tests. Certainly, you should not overuse it. It is best to write as much as possible of your code as pure functions which don't require mocking to test. However, when interacting with the real world (or web services, users etc.) sometimes side-effects are necessary. In these cases, mocking is one useful approach for testing this functionality.

Also, note that Mock has a global effect so if you are using Mocks in multiple tests set async: false so that only one test runs at a time.

Help

Open an issue.

Suggestions

I'd welcome suggestions for improvements or bugfixes. Just open an issue.

mock's People

Contributors

jjh42 avatar olshansk avatar duksis avatar bartj3 avatar superhawk610 avatar mtrudel avatar mattfreer avatar spscream avatar betree avatar bdionne avatar achempion avatar carpodaster avatar connorjacobsen avatar pragdave avatar speeddragon avatar era avatar fracek avatar georgetaveras1231 avatar josephwilk avatar julianespinel avatar jeremyvdw avatar keqi avatar legoscia avatar mbramson avatar styx avatar nrschultz avatar sashman avatar stevedomin avatar amatalai avatar azhi avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.