Skip to content

Commit

Permalink
Complete re-write of Pact
Browse files Browse the repository at this point in the history
This changes pact from being a global registry to being an application
specific dependency registry.
  • Loading branch information
BlakeWilliams committed Jul 20, 2015
1 parent 2b207ea commit 4c9269f
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 178 deletions.
55 changes: 43 additions & 12 deletions README.md
@@ -1,31 +1,62 @@
# Pact

Better dependency injection in Elixir for cleaner code and testing.

Pact is a dependency registry for Elixir to make testing dependencies easier.
## Why?

Because testing Elixir flat out sucks. Why clutter up your code injecting
dependencies when a process can handle it for you?
Because testing Elixir dependencies could be a lot better. Why clutter up your
code injecting dependencies when a process can handle it for you?

* You can declare your modules instead of passing them around like state.
* You can override dependencies per process to make testing easier.
* It makes your code look a lot cleaner.
* You can replace dependencies in a block context for easy testing.
* It makes your code cleaner.

## Usage

In your application code:

```elixir
Pact.start
Pact.put("string", String)
defmodule MyApp.Pact do
use Pact

register "http", HTTPoison
end

MyApp.Pact.start_link

defmodule MyApp.Users do
def all do
MyApp.Pact.get("http").get!("http://foobar.com/api/users")
end
end

```

In your tests:

```
defmodule MyApp.UserTest do
use ExUnit.Case
require MyApp.Pact
test "requests the corrent endpoint" do
fakeHTTP = MyApp.Pact.generate :http do
def get(url) do
send self(), {:called, url}
end
end
Pact.get("string").to_atom("xyz") # => :xyz
MyApp.Pact.replace "http", fakeHTTP do
MyApp.Users.all
end
Pact.override(self, "string", Integer)
Pact.get("string").parse("1234") # => {1234, ""}
assert_receive {:called, "http://foobar.com/api/users"}
end
end
```

You can find more information in the [documentation].

[documentation]: http://hexdocs.pm/pact/0.0.1/
[documentation]: http://hexdocs.pm/pact

## Disclaimer

Expand Down
201 changes: 93 additions & 108 deletions lib/pact.ex
@@ -1,146 +1,131 @@
defmodule Pact do
@moduledoc """
A module for managing dependencies in your application. You can set, get, and
override dependencies globally or per-pid.
A module for managing dependecies in your applicaiton without having to
"inject" dependencies all the way down your aplication. Pact allows you to
set and get dependencies in your application code, and generate fakes and
replace modules in your tests.
## Example
To use Pact, define a module in your application that has `use Pact` in it,
and then call `start_link` on it to start registering your dependencies.
```
Pact.start
Pact.put(:http, HTTPoison)
Pact.get(:http).get("https://google.com")
# You can also override per module
## Usage
Pact.override(self, :http, FakeHTTP)
spawn(fn ->
Pact.get(:http).get("https://google.com") # Calls HTTPoison
end)
Pact.get(:http).get("https://google.com") # Calls FakeHTTP
```
"""
defmodule MyApp.Pact do
use Pact
use GenServer
register "http", HTTPoison
end
@doc """
Replace the given `name` for `pid` with the given expression. This will
generate a new module with the given methods.
MyApp.Pact.start_link
## Example
defmodule MyApp.Users do
def all do
MyApp.Pact.get("http").get("http://foobar.com/api/users")
end
end
```
```
import Pact
Then in your tests you can use Pact to replace the module easily:
Pact.put(:enum, Enum)
Pact.replace self, :enum do
def map(_map, _fn) do
[1, 2, 3]
```
defmodule MyAppTest do
use ExUnit.Case
require MyApp.Pact
test "requests the corrent endpoint" do
fakeHTTP = MyApp.Pact.generate :http do
def get(url) do
send self(), {:called, url}
end
end
end
```
So now if you call `Pact.get(:enum).map(%{}, fn -> end)` it will return
`[1, 2, 3]`.
"""
defmacro replace(pid, name, expression) do
body = Keyword.get(expression, :do)
uid = :base64.encode(:crypto.strong_rand_bytes(5))
module_name = Module.concat([Pact, Fakes, name, uid])
module = Module.create(module_name, body, Macro.Env.location(__ENV__))
MyApp.Pact.replace "http", fakeHTTP do
MyApp.Users.all
end
quote do
Pact.override(unquote(pid), unquote(name), unquote(module_name))
assert_receive {:called, "http://foobar.com/api/users"}
end
end
```
def start(initial_modules\\ %{}) do
modules = %{modules: initial_modules, overrides: %{}}
GenServer.start(__MODULE__, modules, name: __MODULE__)
end

@doc "Gets the dependency with `name`"
def get(name) do
name = to_string(name)
GenServer.call(__MODULE__, {:get, name})
end
## Functions / Macros
@doc "Assign `module` to the key `name`"
def put(name, module) do
name = to_string(name)
GenServer.cast(__MODULE__, {:put, name, module})
end
* `generate(name, block)` - Generates an anonymous module that's body is
block`.
* `replace(name, module, block)` - Replaces `name` with `module` in the given
`block` only.
* `register(name, module)` - Registers `name` as `module`.
* `get(name)` - Get registed module for `name`.
"""

@doc "Override all calls to `name` in `pid` with `module`"
def override(pid, name, module) do
name = to_string(name)
GenServer.cast(__MODULE__, {:override, pid, name, module})
end
defmacro __using__(_) do
quote do
import Pact
use GenServer

@doc "Remove override from process"
def remove_override(pid, name) do
name = to_string(name)
GenServer.cast(__MODULE__, {:remove_override, pid, name})
end
@modules %{}
@before_compile Pact

@doc "Stop Pact"
def stop do
GenServer.call(__MODULE__, :stop)
end
defmacro generate(name, do: block) do
string_name = to_string(name)
uid = :base64.encode(:crypto.strong_rand_bytes(5))

# GenServer
module_name = String.to_atom("#{__MODULE__}.Fakes.#{string_name}.#{uid}")
module = Module.create(module_name, block, Macro.Env.location(__ENV__))

def init(container) do
{:ok, container}
end
quote do
unquote(module_name)
end
end

def handle_cast({:put, name, module}, container) do
modules =
container.modules
|> Map.put(name, module)
defmacro replace(name, module, do: block) do
quote do
existing_module = unquote(__MODULE__).get(unquote(name))
unquote(__MODULE__).register(unquote(name), unquote(module))
unquote(block)
unquote(__MODULE__).register(unquote(name), existing_module)
end
end

{:noreply, %{container | modules: modules}}
end
def register(name, module) do
GenServer.cast(__MODULE__, {:register, name, module})
end

def handle_cast({:override, pid, name, module}, container) do
override =
Map.get(container.overrides, pid, %{})
|> Map.put(name, module)
def get(name) do
GenServer.call(__MODULE__, {:get, name})
end

overrides = Map.put(container.overrides, pid, override)
# Genserver implementation

{:noreply, %{container | overrides: overrides}}
end
def init(container) do
{:ok, container}
end

def handle_cast({:remove_override, pid, name}, container) do
override =
Map.get(container.overrides, pid, %{})
|> Map.delete(name)
def handle_cast({:register, name, module}, state) do
modules = Map.put(state.modules, name, module)
{:noreply, %{state | modules: modules}}
end

if Map.size(override) == 0 do
overrides = Map.delete(container.overrides, pid)
else
overrides = Map.put(container.overrides, pid, override)
def handle_call({:get, name}, _from, state) do
module = get_in(state, [:modules, name])
{:reply, module, state}
end
end

{:noreply, %{container | overrides: overrides}}
end

def handle_call({:get, name}, {pid, _ref}, container) do
override = get_in(container.overrides, [pid, name])

if override do
module = override
else
module = Map.get(container.modules, name)
@doc false
defmacro register(name, module) do
quote do
@modules Map.put(@modules, unquote(name), unquote(module))
end

{:reply, module, container}
end

def handle_call(:stop, _from, container) do
{:stop, :normal, :ok, container}
defmacro __before_compile__(_env) do
quote do
def start_link do
GenServer.start_link(__MODULE__, %{modules: @modules}, name: __MODULE__)
end
end
end
end
4 changes: 2 additions & 2 deletions mix.exs
Expand Up @@ -3,7 +3,7 @@ defmodule Pact.Mixfile do

def project do
[app: :pact,
version: "0.1.0",
version: "0.2.0",
elixir: "~> 1.0",
deps: deps,
description: description,
Expand All @@ -24,7 +24,7 @@ defmodule Pact.Mixfile do
end

defp description do
"Better dependency injection in Elixir through inversion of control"
"Elixir dependency registry for better testing and cleaner code"
end

defp package do
Expand Down

0 comments on commit 4c9269f

Please sign in to comment.