Stubr
Stubr is a set of functions helping people to create stubs and spies in Elixir.
About
In Elixir, you should aim to write pure functions. However, sometimes you need to write functions that post to external API’s or functions that depend on the current time. Since these actions can lead to side effects, they can make it harder to unit test your system.
Stubr solves this problem by taking cues from mocks and explicit contracts. It provides a set of functions that help people create "mocks as nouns" and not "mocks as verbs":
iex> stub = Stubr.stub!([foo: fn _ -> :ok end], call_info: true)
iex> stub.foo(1)
iex> stub |> Stubr.called_once?(:foo)
true
iex> spy = Stubr.spy!(Float)
iex> spy.ceil(1.5)
iex> spy |> Stubr.called_with?(:ceil, [1.5])
true
iex> spy |> Stubr.called_twice?(:ceil)
falseInstallation
Stubr is available in Hex, the package can be installed as:
Add stubr to your list of dependencies in mix.exs:
def deps do
[{:stubr, "~> 1.5.0", only: :test}]
endFor versions of elixir less than 1.4 use this package:
def deps do
[{:stubr, "~> 1.4.0", only: :test}]
endDeveloper documentation
Stubr documentation is available in hexdocs.
Examples
Random numbers
It is easy to use Stubr.stub! to set up a stub for the uniform/1 function in the :rand module. Note, there is no need to explicitly set the module option, it is just used to make sure the uniform/1 function exists in the :rand module.
test "create a stub of the :rand.uniform/1 function" do
rand_stub = Stubr.stub!([uniform: fn _ -> 1 end], module: :rand)
assert rand_stub.uniform(1) == 1
assert rand_stub.uniform(2) == 1
assert rand_stub.uniform(3) == 1
assert rand_stub.uniform(4) == 1
assert rand_stub.uniform(5) == 1
assert rand_stub.uniform(6) == 1
endTimex
As above, we can use Stubr.stub! to stub the Timex.now/0 function in the Timex module. However, we also want the stub to defer to the original functionality of the Timex.before?/2 function. To do this, we just set the module option to Timex and the auto_stub option to true.
test "create a stub of Timex.now/0 and defer on all other functions" do
fixed_time = Timex.to_datetime({2999, 12, 30})
timex_stub = Stubr.stub!([now: fn -> fixed_time end], module: Timex, auto_stub: true)
assert timex_stub.now == fixed_time
assert timex_stub.before?(fixed_time, timex_stub.shift(fixed_time, days: 1))
endHTTPoison
In this example, we create stubs of the functions get and post in the HTTPoison module that return different values dependant on their inputs:
setup_all do
http_poison_stub = Stubr.stub!([
get: fn("www.google.com") -> {:ok, %HTTPoison.Response{body: "search", status_code: 200}} end,
get: fn("www.nasa.com") -> {:ok, %HTTPoison.Response{status_code: 401}} end,
post: fn("www.nasa.com", _) -> {:error, %HTTPoison.Error{reason: :econnrefused}} end
], module: HTTPoison)
[stub: http_poison_stub]
end
test "create a stub of HTTPoison.get/1", context do
{:ok, google_response} = context[:stub].get("www.google.com")
{:ok, nasa_response} = context[:stub].get("www.nasa.com")
assert google_response.body == "search"
assert google_response.status_code == 200
assert nasa_response.status_code == 401
end
test "create a stub of HTTPoison.post/2", context do
{:error, error} = context[:stub].post("www.nasa.com", "any content")
assert error.reason == :econnrefused
endLinks
Here is a good guide to TDD in functional languages using stubs: https://www.infoq.com/presentations/mock-fsharp-tdd
Mark Seemann's blog post talks about the difference between Mocks and Stubs in the context of commands and queries.