I just took my hands off the first pass at elixir_mock - a library that creates inspectable test doubles (mocks) for elixir tests. I seek your feedback.
The library creates mocks with the following Characteritics:
- Mocks are defined based on any already defined module
- Mocks are independent of the module they are defined from and don’t modify or replace those modules
- Mocks are also independent of each other so they don’t stop you from running your tests in parallel
- Mocks are inspectable. That is, you can verify that certain functions were called on the mocks with specific arguments
I started working on the library after I read @josevalim’s blog on Mocks and explicit contracts. I learned the pattern of injecting dependencies as optional parameters into functions and using Mix.ENV
to swap out real implementations for test modules during tests and it was awesome.
However, for tests that test my app’s interaction with the outside world, I found that having a single, large test double module wouldn’t suffice. I needed a concise way to create these test doubles based on any “real world” module right within my tests. I also wanted these mock modules to be inspectable so I could ensure that my app interacted with the “outside world” in the right way.
So you don’t have to click through, here’s a sample of the API I came up with:
defmodule MyTest do
use ExUnit.Case, async: true # yes, you can run test that use mocks in parallel with any other tests
require ElixirMock
alias ElixirMock.Matchers
test "should create a mock based on the inbuilt List module" do
# create mock with same API as List module.
list_mock = ElixirMock.mock_of List
"""
Call a function on the mock. This will typically be done by another module under test
that takes the `list_mock` as a dependency. That detail is omitted here for brevity.
"""
list_mock.first([1, 2])
# verify what calls were made to the mock.
assert_called list_mock.first([1, 2]) # passes
refute_called list_mock.first([]) # passes because `first` wasn't called with an empty list
assert_called list_mock.first(Matchers.any(:list)) # passes
assert_called list_mock.first(Matchers.any(:integer)) # fails!
end
end
It will be very helpful to hear from the community here about what they think about:
- Whether or not the approach of creating injectable and inspectable mock modules is good or has any serious shortfalls
- The way the library’s API is designed
- And if you have time, feedback about the implementation (code)
- Anything else you feel is important to mention.
Thanks a lot!