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 theElixirMock.defmock_of/2
orElixirMock.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 not
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 a
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
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.