Skip to content

Commit

Permalink
Merge 51b201b into a42993a
Browse files Browse the repository at this point in the history
  • Loading branch information
amatalai committed Aug 5, 2017
2 parents a42993a + 51b201b commit 8ea275f
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 37 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

.tool-versions
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Changelog

## 1.1.0
* Added `Mockery.mock/2` (`Mockery.mock/3` value defaults to `:mocked`)
* Added `Mockery.Assertions.assert_called/4` macro
* Added `Mockery.Assertions.refute_called/4` macro

## 1.0.1
* Fixed issue when mocking with `nil` or `false`

## 1.0.0
* Added `use Mockery` for importing both `Mockery` and `Mockery.Assertions`
* Added `refute_called/2` and `refute_called/3`
* Added `Mockery.Assertions.refute_called/2` and `Mockery.Assertions.refute_called/3`
* Loosen elixir version requirement from `~> 1.3` to `~> 1.1`
55 changes: 53 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ end

## Basic usage

Mock
#### Static value mock

```elixir
# prepare tested module
Expand All @@ -49,9 +49,14 @@ assert filtered() == "mock"
mock MyApp.UserService, [users: 0], "mock"
assert all() == "mock"
refute filtered() == "mock"

# mock MyApp.UserService.users/0 with implicit value
mock MyApp.UserService, users: 0
assert all() == :mocked
refute filtered() == :mocked
```

Dynamic mock
#### Dynamic mock

```elixir
defmodule Foo do
Expand Down Expand Up @@ -101,8 +106,25 @@ assert_called Foo, :bar, [1, %{}]
# assert Foo.bar/2 was called with 1 as first arg
call(1, %{})
assert_called Foo, :bar, [1, _]

# assert Foo.bar/2 was called with 1 as first arg 5 times
# ...
assert_called Foo, :bar, [1, _], 5

# assert Foo.bar/2 was called with 1 as first arg from 3 to 5 times
# ...
assert_called Foo, :bar, [1, _], 3..5

# assert Foo.bar/2 was called with 1 as first arg 3 or 5 times
# ...
assert_called Foo, :bar, [1, _], [3, 5]
```

#### Refute

Every assert_called/x function/macro has its refute_called/x counterpart.<br>
For more information see [docs](https://hexdocs.pm/mockery/Mockery.Assertions.html)

## Global mock

```elixir
Expand Down Expand Up @@ -133,6 +155,35 @@ assert index() == [:user1, :user2, :user3]

For advanced usage examples see [EXAMPLES.md](EXAMPLES.md)

## Philosophy behind this project

#### Nothing is shared

Mocks and function call history are stored separately for every test in
its own process dictionary.

#### Don't use modules as additional function argument

When you use functions like this:

```elixir
def something(foo \\ Foo) do
foo.bar()
end
```

You loose some compilation warnings. It was always unacceptable for me.
After changing function name I expect that project recompilation will show me
if old name is still used somewhere in my code by throwing `function Foo.bar/0 is
undefined or private` straight into my face. With `Mockery` it's no longer an issue.

#### Don't create custom macros when it's not necessary

To prepare module for mocking we decided to use module attributes instead of
any magic macros that changes code behind the scene.<br>
In our company we believe that any new person joining project should be able
to understand existing code immediately.

## License

Copyright 2017 Tobiasz Małecki <tobiasz.malecki@appunite.com>
Expand Down
2 changes: 2 additions & 0 deletions lib/mockery.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ defmodule Mockery do
doesn't make any sense, because it will only work for Mod.fun/1.
"""
def mock(mod, fun, value \\ :mocked)

def mock(mod, fun, nil),
do: Process.put(Utils.dict_mock_key(mod, fun), Mockery.Nil)
def mock(mod, fun, false),
Expand Down
101 changes: 97 additions & 4 deletions lib/mockery/assertions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ defmodule Mockery.Assertions do
"""
defmacro assert_called(mod, fun, args) do
quote do
ExUnit.Assertions.assert unquote(called_with?(mod, fun, args)), unquote(message(mod, fun))
ExUnit.Assertions.assert unquote(called_with?(mod, fun, args)), unquote(failure(mod, fun))
end
end

Expand All @@ -95,7 +95,69 @@ defmodule Mockery.Assertions do
"""
defmacro refute_called(mod, fun, args) do
quote do
ExUnit.Assertions.refute unquote(called_with?(mod, fun, args)), unquote(refute_message(mod, fun))
ExUnit.Assertions.refute unquote(called_with?(mod, fun, args)), unquote(refute_failure(mod, fun))
end
end

@doc """
Asserts that function from given module with given name was called
given number of times with arguments matching given pattern.
Similar to `assert_called/3` but instead of checking if function was called
at least once, it checks if function was called specific number of times.
**NOTE**: Mockery doesn't keep track of function calls on modules that
weren't prepared by `Mockery.of/2` and for MIX_ENV other than :test
## Examples
Assert Mod.fun/2 was called with given args 5 times
assert_called Mod, :fun, ["a", "b"], 5
Assert Mod.fun/2 was called with given args from 3 to 5 times
assert_called Mod, :fun, ["a", "b"], 3..5
Assert Mod.fun/2 was called with given args 3 or 5 times
assert_called Mod, :fun, ["a", "b"], [3, 5]
"""
defmacro assert_called(mod, fun, args, times) do
quote do
ExUnit.Assertions.assert unquote(ncalled_with?(mod, fun, args, times)), unquote(nfailure(mod, fun))
end
end

@doc """
Asserts that function from given module with given name was NOT called
given number of times with arguments matching given pattern.
Similar to `refute_called/3` but instead of checking if function was called
at least once, it checks if function was called specific number of times.
**NOTE**: Mockery doesn't keep track of function calls on modules that
weren't prepared by `Mockery.of/2` and for MIX_ENV other than :test
## Examples
Assert Mod.fun/2 was not called with given args 5 times
refute_called Mod, :fun, ["a", "b"], 5
Assert Mod.fun/2 was not called with given args from 3 to 5 times
refute_called Mod, :fun, ["a", "b"], 3..5
Assert Mod.fun/2 was not called with given args 3 or 5 times
refute_called Mod, :fun, ["a", "b"], [3, 5]
"""
defmacro refute_called(mod, fun, args, times) do
quote do
ExUnit.Assertions.refute unquote(ncalled_with?(mod, fun, args, times)), unquote(nrefute_failure(mod, fun))
end
end

Expand All @@ -114,15 +176,46 @@ defmodule Mockery.Assertions do
end
end

defp message(mod, fun) do
defp ncalled_with?(mod, fun, args, times) when is_integer(times) do
quote do
unquote(mod)
|> Utils.get_calls(unquote(fun))
|> Enum.filter(&match?({_, unquote(args)}, &1))
|> Enum.count()
|> (& (&1 == unquote(times))).()
end
end
defp ncalled_with?(mod, fun, args, times) do
quote do
unquote(mod)
|> Utils.get_calls(unquote(fun))
|> Enum.filter(&match?({_, unquote(args)}, &1))
|> Enum.count()
|> (& (&1 in unquote(times))).()
end
end

defp failure(mod, fun) do
quote do
"#{unquote(mod)}.#{unquote(fun)} was not called with given arguments"
end
end

defp refute_message(mod, fun) do
defp nfailure(mod, fun) do
quote do
"#{unquote(mod)}.#{unquote(fun)} was not called with given arguments expected number of times"
end
end

defp refute_failure(mod, fun) do
quote do
"#{unquote(mod)}.#{unquote(fun)} was called with given arguments at least once"
end
end

defp nrefute_failure(mod, fun) do
quote do
"#{unquote(mod)}.#{unquote(fun)} was called with given arguments unexpected number of times"
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Mockery.Mixfile do
use Mix.Project

@version "1.0.1"
@version "1.1.0"

def project do
[
Expand Down
Loading

0 comments on commit 8ea275f

Please sign in to comment.