Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/jechol/definject
Unobtrusive Dependency Injector for Elixir
https://github.com/jechol/definject
dependency-injection di elixir hacktoberfest mock unobtrusive
Last synced: about 2 months ago
JSON representation
Unobtrusive Dependency Injector for Elixir
- Host: GitHub
- URL: https://github.com/jechol/definject
- Owner: jechol
- License: other
- Created: 2020-02-28T04:32:22.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2024-04-10T12:01:58.000Z (2 months ago)
- Last Synced: 2024-04-26T07:11:40.320Z (about 2 months ago)
- Topics: dependency-injection, di, elixir, hacktoberfest, mock, unobtrusive
- Language: Elixir
- Homepage:
- Size: 174 KB
- Stars: 57
- Watchers: 3
- Forks: 3
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Lists
- awesome-elixir - definject - Unobtrusive dependency injector for Elixir. (Testing)
README
![](https://github.com/jechol/definject/blob/master/brand/logo.png?raw=true)
[![mix test](https://github.com/jechol/definject/workflows/mix%20test/badge.svg)](https://github.com/jechol/definject/actions)
[![Hex version badge](https://img.shields.io/hexpm/v/definject.svg)](https://hex.pm/packages/definject)
[![License badge](https://img.shields.io/hexpm/l/definject.svg)](https://github.com/jechol/definject/blob/master/LICENSE.md)Unobtrusive Dependency Injector for Elixir
## Why?
Let's say we want to test following function.
```elixir
def send_welcome_email(user_id) do
%{email: email} = Repo.get(User, user_id)welcome_email(to: email)
|> Mailer.send()
end
```Here's one possible solution to replace `Repo.get/2` and `Mailer.send/1` with mocks:
```elixir
def send_welcome_email(user_id, repo \\ Repo, mailer \\ Mailer) do
%{email: email} = repo.get(User, user_id)welcome_email(to: email)
|> mailer.send()
end
```First, I believe that this approach is too obtrusive as it requires modifying the function body to make it testable. Second, with `Mailer` replaced with `mailer`, the compiler no longer check the existence of `Mailer.send/1`.
`definject` does not require you to modify function arguments or body. It allows injecting different mocks to each function. It also does not limit using `:async` option as mocks are contained in each test function.
## Installation
The package can be installed by adding `definject` to your list of dependencies
in `mix.exs`:```elixir
def deps do
[{:definject, "~> 1.2"}]
end
```By default, `definject` is replaced with `def` in all but the test environment. Add the below configuration to enable in other environments.
```elixir
config :definject, :enable, true
```To format `definject` like `def`, add following to your `.formatter.exs`
```elixir
locals_without_parens: [definject: 1, definject: 2]
```## Documentation
API documentation is available at [https://hexdocs.pm/definject](https://hexdocs.pm/definject)
## Usage
### use Definject
`use Definject` transforms `def` to accept a extra argument `deps` where dependent functions and modules can be injected.
```elixir
use Definjectdef send_welcome_email(user_id) do
%{email: email} = Repo.get(User, user_id)welcome_email(to: email)
|> Mailer.send()
end
```is expanded into
```elixir
def send_welcome_email(user_id, deps \\ %{}) do
%{email: email} =
Map.get(deps, &Repo.get/2,
:erlang.make_fun(Map.get(deps, Repo, Repo), :get, 2)
).(User, user_id)welcome_email(to: email)
|> Map.get(deps, &Mailer.send/1,
:erlang.make_fun(Map.get(deps, Mailer, Mailer), :send, 1)
).()
end
```Note that local function calls like `welcome_email(to: email)` are not expanded unless it is prepended with `__MODULE__`.
Now, you can inject mock functions and modules in tests.
```elixir
test "send_welcome_email" do
Accounts.send_welcome_email(100, %{
Repo => MockRepo,
&Mailer.send/1 => fn %Email{to: "[email protected]", subject: "Welcome"} ->
Process.send(self(), :email_sent)
end
})assert_receive :email_sent
end
```Function calls raise if the `deps` includes redundant functions or modules.
You can disable this by adding `strict: false` option.```elixir
test "send_welcome_email with strict: false" do
Accounts.send_welcome_email(100, %{
&Repo.get/2 => fn User, 100 -> %User{email: "[email protected]"} end,
&Repo.all/1 => fn _ -> [%User{email: "[email protected]"}] end, # Unused
strict: false
})
end
```### mock
If you don't need pattern matching in mock function, `mock/1` can be used to reduce boilerplates.
```elixir
import Definjecttest "send_welcome_email with mock/1" do
Accounts.send_welcome_email(
100,
mock(%{
Repo => MockRepo,
&Mailer.send/1 => Process.send(self(), :email_sent)
})
)assert_receive :email_sent
end
```Note that `Process.send(self(), :email_sent)` is surrounded by `fn _ -> end` when expanded.
### import Definject
`import Definject` instead of `use Definject` if you want to manually select functions to inject.
```elixir
import Definjectdefinject send_welcome_email(user_id) do
%{email: email} = Repo.get(User, user_id)welcome_email(to: email)
|> Mailer.send()
end
```## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details