/stubr

Stubr is a set of functions helping people to create stubs and spies in Elixir.

Primary LanguageElixirMIT LicenseMIT

Stubr

Build Status

Stubr is a set of functions helping people to create stubs and spies in Elixir.

About

Elixir is a functional language, so you should aim to write pure functions. However, sometimes you need to call external API’s or check the current time. Since these actions can have side effects, they 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)
false

Installation

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.1", only: :test}]
end

Developer documentation

Stubr documentation is available in hexdocs.

Examples

Random numbers

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, however, it is useful to do so because it makes 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
end

Timex

As above, use Stubr.stub! to stub the Timex.now/0 function in the Timex module. However, we also want the stub to act as a transparent proxy over the Timex module for all non-stubbed functions. 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))
end

HTTPoison

In this example, we create stubs of the functions get and post in the HTTPoison module and make them return different values based 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
end

Links

TDD in functional languages using stubs: https://www.infoq.com/presentations/mock-fsharp-tdd

Mark Seemann's blog post about the difference between Mocks and Stubs in the context of commands and queries.