Skip to content

wanderanimrod/elixir_mock

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ElixirMock

CircleCI Build Status Hex pm

Creates inspectable mocks (test doubles) based on real elixir modules for testing.

The mocks do not replace or modify the original modules the are based on and are fully independent of each other. Because of this isolation, mocks defined from the same real module can be used in multiple tests running in parallel. Also, tests using mocks defined from a real module can run in parallel with other tests that depend on the real module.

Installation

Available in Hex. The package can be installed by adding elixir_mock to your list of dependencies in mix.exs:

def deps do
  [{:elixir_mock, "~> 0.2.9", only: :test}] # or whatever the latest version is
end

Getting started

ElixirMock helps you create inspectable test doubles (mocks) for use within your ExUnit tests.

ElixirMock mocks are just modules defined based on other modules ("real modules"). The mocks are created at compile-time by copying public functions from the real module to the mock module, with a few modifications that allow calls to the mock's functions to be recorded and inspected later by tests.

The mocks do not replace or modify in any way the modules they are based on. They are only meant to be injected as dependencies into functions under test.

Example

The example below demonstrates how you would test that a module in your app, MyApp.User, makes a call to the facebook api with the right parameters and returns to you whatever the facebook api returns.

First, let's define a module that wraps the Facebook API. This could be your own module, a module from the standard library, or one from a hex package.

defmodule FacebookClient do
  def get_profile(profile_id) do
    # makes api call to facebook
    :real_facebook_user_profile
  end
end

Next, we define a module in our app that we are going to test. This is the module that will go through the FacebookClient to fetch the user's profile from Facebook

defmodule MyApp.User do
  # allow function under test to accept injected api client dependency
  def load_user(user_id, facebook_client \\ FacebookClient) do
    facebook_client.get_profile(user_id)
  end
end

Now we are ready to test our MyApp.User module's load_user/2 functionality. We will do this without actually hitting the Facebook API but while still verifying that our code interacts with the api correctly.

defmodule MyApp.UserTest do
  use ExUnit.Case, async: true # yes, you can run tests that use mocks in parallel
  require ElixirMock
  import ElixirMock

  test "should get user profile from facebook api when user is loaded" do
    # create mock module with the same functions as the `FacebookClient` module.
    mock_facebook_client = mock_of FacebookClient

    # Call the function you are testing, injecting the mock FacebookClient
    user = MyApp.User.load_user("some-user-id", mock_facebook_client)

    # Check that facebook was called with the user id we passed to MyApp.User.load_user/1
    assert_called mock_facebook_client.get_profile("some-user-id") # passes
  end

end

We can even go a step further. We can test that the MyApp.User.load_user/1 function actually returns what the facebook api returns. To do this, we fix the responses from the Facebook API using the ElixirMock.defmock_of/2 macro and check that our code returns those fixed responses.

defmodule MyApp.UserTest do
  use ExUnit.Case, async: true
  require ElixirMock
  import ElixirMock

  test "load_user/1 returns the response from facebook without any processing." do
    with_mock(mock_facebook_client) = defmock_of FacebookClient do
      def get_profile(_), do: "a custom response from the mock"
    end
    user = MyApp.User.load_user("some-user-id", mock_facebook_client)
    assert user == "a custom response from the mock" # passes
  end
end

There's plenty more ElixirMock can do. Please refer to the rest of this page for a gentle introduction to ElixirMock or jump right into the docs to discover more hidden treasures.

Characteristics of mocks

  • Every mock module has a unique, random UUID atom as its name. You can use ElixirMock.with_mock/1 to give your mock a fixed human-friendly name.
  • All functions on a mock return nil unless otherwise specified with the ElixirMock.defmock_of/2 or ElixirMock.defmock_of/3 macros.
  • A new mock module is created each time a mock definition is used. Each mock is completely independent of other mocks and does not replace or affect the real module it is based on in any way. This allows you to run tests that make use of mocks in parallel with other tests that use the real modules the mocks are based on, or other tests using mocks based on the same real modules.
  • All calls to functions on a mock are recorded by function name and the arguments passed to that function in the call.
  • Only public functions are copied from the parent module into the mock. Macros and module attributes are not copied.

Types of mocks

There are currently two kinds of mocks you can define with ElixirMock

The simple mock

Simple mocks are defined using the ElixirMock.mock_of/1 function. They are based on already existing modules (referred to as "parent modules"). Once defined, these mocks inherit all functions defined on the parent module with their implementations stubbed out to return nil. They are called simple mocks because they do not specify any special behaviour for the mock's functions.

Example:

Creating a mock module that has the same api as the in-built elixir List module but with its functions returning nil.

require ElixirMock
import ElixirMock

list_mock = mock_of List
list_mock.first([1, 2]) == nil
#=> true

list_mock.last([1, 2]) == nil
#=> true

You can also define the mock to delegate all calls to the real module if you want to record calls to the functions but no alter their behaviour. See the ElixirMock.mock_of/1 documentation for an example of this and other options.

Custom mocks

ElixirMock also allows you to define mocks that override some or all of the functions inherited from the module the mocks are based on. This is done using the ElixirMock.defmock_of/2 and ElixirMock.defmock_of/3 macros.

Example: Creating a mock of the inbuilt List module and overriding its List.first/1 function.

require ElixirMock
import ElixirMock

with_mock(list_mock) = defmock_of List do
  def first(_list), do: :mock_implementation
end

list_mock.first([1, 2]) == :mock_implementation
#=> true

For more details on the options available within custom mock definitions, see ElixirMock.defmock_of/2 and ElixirMock.defmock_of/3 documentation.

Verifying calls on mocks

The ElixirMock.assert_called/1 and ElixirMock.refute_called/1 macros allow you to verify which calls were made to mock and which arguments were passed when those calls were made.

These macros take in an expression that looks exactly like the function call you expect to have or not have been made. The function call expressions passed are not executed. Rather, they are deconstructed to get the function name and the arguments. The function name and arguments are then used to find the call in the mocks recorded list of calls.

Example:

defmodule MyTest do
  use ExUnit.Case, async: true
  require ElixirMock
  import ElixirMock

  test "verifies that function on mock was called" do
    mock = mock_of List

    mock.first [1, 2]

    assert_called mock.first([1, 2]) # passes
    refute_called mock.first(:some_other_arg) # passes
  end
end

For more details on how to do assertions against mocks, see the ElixirMock.assert_called/1, ElixirMock.refute_called/1, and ElixirMock.Matchers documentation pages.

Managing mock state

While the use of the inbuilt ElixirMock.assert_called/1 and ElixirMock.refute_called/1 macros is encouraged for all simple cases, there are cases where access to the raw data stored in the mock is necessary. For those special cases, ElixirMock allows you to interrogate mocks for details on what calls were made to them and what arguments were passed when those calls were made. For further details on how to do this, please refer to the ElixirMock.Mock module documentation.

Contributing

Should you enjoy using this package, please let me know @wanderanimrod. If you don't like it for good reasons, please let me know too. If you find a bug or have a feature or pull request, please create an issue on github and I'll be glad to help.