Skip to content

Commit

Permalink
Merge branch 'master' of github.com:sonerdy/double
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon Joyce committed Apr 22, 2019
2 parents 5ff09e6 + 8daa8f3 commit 2d579c2
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 71 deletions.
111 changes: 47 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ It does NOT override behavior of existing modules or functions.
Double uses Elixir's built-in language features such as pattern matching and message passing to
give you everything you would normally need a complex mocking tool for.

Checkout [Testing Elixir: The Movie](https://youtu.be/cyU_SFyVRro) for a fun introduction to Double and unit testing in Elixir.

## Installation
The package can be installed as:

1. Add `double` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[{:double, "~> 0.6.6", only: :test}]
[{:double, "~> 0.7.0", only: :test}]
end
```

Expand All @@ -26,14 +24,14 @@ Application.ensure_all_started(:double)
```

### Module/Behaviour Doubles
Double creates a fake module based off of a behaviour or another module.
Double creates a fake module based off of a behaviour or module.
You can use this module like any other module that you call functions on.
Each stub you define will verify that the function name and arity are defined in the target module or behaviour.

```elixir
defmodule Example do
def process(io \\ IO) do # allow an alternative dependency to be passed
io.puts("It works without mocking libraries")
io.puts("It works without mocking libraries!")
end
end

Expand All @@ -42,14 +40,12 @@ defmodule ExampleTest do
import Double

test "example outputs to console" do
stub = IO
|> double()
|> allow(:puts, fn(_msg) -> :ok end)
io_stub = stub(IO,:puts, fn(_msg) -> :ok end)

Example.process(stub) # inject the stub module
Example.process(io_stub) # inject the stub module

# use built-in ExUnit assert_receive/refute_receive to verify things
assert_receive({:puts, "It works without mocking libraries"})
assert_receive({IO, :puts, "It works without mocking libraries!"})
end
end
```
Expand All @@ -58,100 +54,87 @@ end
### Basics
```elixir
# Stub a function
stub = ExampleModule
|> double()
|> allow(:add, fn(x, y) -> x + y end)
stub.add(2, 2) # 4
dbl = stub(ExampleModule, :add, fn(x, y) -> x + y end)
dbl.add(2, 2) # 4

# Pattern match arguments
stub = Application
|> double()
|> allow(:ensure_all_started, fn(:logger) -> nil end)
stub.ensure_all_started(:logger) # nil
stub.ensure_all_started(:something) # raises FunctionClauseError
dbl = stub(Application, :ensure_all_started, fn(:logger) -> nil end)
dbl.ensure_all_started(:logger) # nil
dbl.ensure_all_started(:something) # raises FunctionClauseError

# Stub as many functions as you want
stub = ExampleModule
|> double()
|> allow(:add, fn(x, y) -> x + y end)
|> allow(:subtract, fn(x, y) -> x - y end)
dbl = ExampleModule
|> stub(:add, fn(x, y) -> x + y end)
|> stub(:subtract, fn(x, y) -> x - y end)
```

### Different return values for different arguments
```elixir
stub = ExampleModule
|> double()
|> allow(:example, fn("one") -> 1 end)
|> allow(:example, fn("two") -> 2 end)
|> allow(:example, fn("three") -> 3 end)

stub.example("one") # 1
stub.example("two") # 2
stub.example("three") # 3
dbl = ExampleModule
|> stub(:example, fn("one") -> 1 end)
|> stub(:example, fn("two") -> 2 end)
|> stub(:example, fn("three") -> 3 end)

dbl.example("one") # 1
dbl.example("two") # 2
dbl.example("three") # 3
```

### Multiple calls returning different values
```elixir
stub = ExampleModule
|> double()
|> allow(:example, fn("count") -> 1 end)
|> allow(:example, fn("count") -> 2 end)

stub.example("count") # 1
stub.example("count") # 2
stub.example("count") # 2
dbl = ExampleModule
|> stub(:example, fn("count") -> 1 end)
|> stub(:example, fn("count") -> 2 end)

dbl.example("count") # 1
dbl.example("count") # 2
dbl.example("count") # 2
```

### Exceptions
```elixir
stub = ExampleModule
|> double()
|> allow(:example_with_error_type, fn -> raise RuntimeError, "kaboom!" end)
|> allow(:example_with_error_type, fn -> raise "kaboom!" end)
dbl = ExampleModule
|> stub(:example_with_error_type, fn -> raise RuntimeError, "kaboom!" end)
|> stub(:example_with_error_type, fn -> raise "kaboom!" end)
```

### Verifying calls
If you want to verify that a particular stubbed function was actually executed,
Double ensures that a message is receivable to your test process so you can just use the built-in ExUnit `assert_receive/assert_received`.
The message is a tuple starting with the function name, and then the arguments received.
The message is a 3-tuple `{module, :function, [arg1, arg2]}`and .

```elixir
stub = ExampleModule
|> double()
|> allow(:example, fn("count") -> 1 end)
stub.example("count")
assert_receive({:example, "count"})
dbl = ExampleModule
|> stub(:example, fn("count") -> 1 end)
dbl.example("count")
assert_receive({ExampleModule, :example, "count"})
```
Remember that pattern matching is your friend so you can do all kinds of neat tricks on these messages.
```elixir
assert_receive({:example, "c" <> _rest}) # verify starts with "c"
assert_receive({:example, %{test: 1}) # pattern match map arguments
assert_receive({:example, x}) # assign an argument to x to verify another way
assert_receive({ExampleModule, :example, "c" <> _rest}) # verify starts with "c"
assert_receive({ExampleModule, :example, %{test: 1}) # pattern match map arguments
assert_receive({ExampleModule, :example, x}) # assign an argument to x to verify another way
assert x == "count"
```

### Module Verification
By default your setups will check the source module to ensure the function exists with the correct arity.

```elixir
IO
|> double()
|> allow(:non_existent_function, fn(x) -> x end) # raises VerifyingDoubleError
stub(IO, :non_existent_function, fn(x) -> x end) # raises VerifyingDoubleError
```

### Clearing Stubs
Occasionally it's useful to clear the stubs for an existing double. This is useful when you have
a shared setup and a test needs to change the way a double is stubbed without recreating the whole thing.

```elixir
stub = IO
|> double()
|> allow(:puts, fn(_) -> :ok end)
|> allow(:inspect, fn(_) -> :ok end)
dbl = IO
|> stub(:puts, fn(_) -> :ok end)
|> stub(:inspect, fn(_) -> :ok end)

# later
stub |> clear(:puts) # clear an individual function
stub |> clear([:puts, :inspect]) # clear a list of functions
stub |> clear() # clear all functions
dbl |> clear(:puts) # clear an individual function
dbl |> clear([:puts, :inspect]) # clear a list of functions
dbl |> clear() # clear all functions
```

45 changes: 39 additions & 6 deletions lib/double.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule Double do
alias Double.FuncList
use GenServer

@default_options [verify: true]
@default_options [verify: true, send_stubbed_module: false]

@type allow_option ::
{:with, [...]}
Expand All @@ -22,6 +22,25 @@ defmodule Double do

# API

@spec stub(atom, atom, function) :: atom
def stub(dbl), do: double(dbl, Keyword.put(@default_options, :send_stubbed_module, true))

def stub(dbl, function_name, func) do
double_id = Atom.to_string(dbl)
pid = Registry.whereis_double(double_id)

dbl =
case pid do
:undefined -> stub(dbl)
_ -> dbl
end

dbl
|> verify_mod_double(function_name, func)
|> verify_struct_double(function_name)
|> do_allow(function_name, func)
end

@spec double :: map
@spec double(struct, [double_option]) :: struct
@spec double(atom, [double_option]) :: atom
Expand Down Expand Up @@ -219,13 +238,17 @@ defmodule Double do
{function_name, arity(func)}
end)

opts = Registry.opts_for("#{mod}")
stubbed_module = Registry.source_for("#{mod}")

code = """
defmodule :#{mod} do
"""

code =
Enum.reduce(funcs, code, fn {function_name, func}, acc ->
{signature, message} = function_parts(function_name, func)
{signature, message} =
function_parts(function_name, func, {opts[:send_stubbed_module], stubbed_module})

acc <>
"""
Expand All @@ -241,7 +264,7 @@ defmodule Double do
end

defp stub_function(double_id, function_name, func) do
{signature, message} = function_parts(function_name, func)
{signature, message} = function_parts(function_name, func, {false, nil})

func_str = """
fn(#{signature}) ->
Expand All @@ -263,7 +286,7 @@ defmodule Double do
"""
end

defp function_parts(function_name, func) do
defp function_parts(function_name, func, {send_stubbed_module, stubbed_module}) do
signature =
case arity(func) do
0 ->
Expand All @@ -276,8 +299,9 @@ defmodule Double do
end

message =
case signature do
"" -> ":#{function_name}"
case {send_stubbed_module, signature} do
{true, _} -> "{#{atom_to_code_string(stubbed_module)}, :#{function_name}, [#{signature}]}"
{false, ""} -> ":#{function_name}"
_ -> "{:#{function_name}, #{signature}}"
end

Expand Down Expand Up @@ -344,4 +368,13 @@ defmodule Double do
defp quoted_fn_body(opts, nil) do
opts[:returns]
end

defp atom_to_code_string(atom) do
atom_str = Atom.to_string(atom)

case String.downcase(atom_str) do
^atom_str -> ":#{atom_str}"
_ -> atom_str
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Double.Mixfile do
use Mix.Project
@version "0.6.6"
@version "0.7.0"

def project do
[
Expand Down
Loading

0 comments on commit 2d579c2

Please sign in to comment.