Code Monkey home page Code Monkey logo

definject's Introduction

mix test Hex version badge License badge

Unobtrusive Dependency Injector for Elixir

Why?

Let's say we want to test following function.

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:

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:

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.

config :definject, :enable, true

To format definject like def, add following to your .formatter.exs

locals_without_parens: [definject: 1, definject: 2]

Documentation

API documentation is available at 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.

use Definject

def send_welcome_email(user_id) do
  %{email: email} = Repo.get(User, user_id)

  welcome_email(to: email)
  |> Mailer.send()
end

is expanded into

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.

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.

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.

import Definject

test "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.

import Definject

definject 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 file for details

definject's People

Contributors

harfangk avatar jechol avatar nallwhy avatar renovate-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

definject's Issues

Problem of "no do" case

When you change the code like below and run your test, CompileError happens at "no do" case.
(or just run $ env MIX_ENV=dev mix test)

And it means that error will happen on other environments except 'test'.

  # definject.ex:70

  defp do_definject(head, body_and_resq, %Macro.Env{} = env) do
    alias Definject.Inject

    original =
      quote do
        def unquote(head), unquote(body_and_resq)
      end

    # if Application.get_env(:definject, :enable, Mix.env() == :test) do
    #   Inject.inject_function(head, body_and_resq, env)
    #   |> trace(original, env)
    # else
      original
    # end
  end
== Compilation error in file test/use_cases/no_do_test.exs ==
** (CompileError) test/use_cases/no_do_test.exs:6: missing :do option in "def"
    test/use_cases/no_do_test.exs:6: (module)
    test/use_cases/no_do_test.exs:5: (module)
    (stdlib 3.13) erl_eval.erl:680: :erl_eval.do_apply/6
    (elixir 1.10.4) lib/kernel/parallel_compiler.ex:396: Kernel.ParallelCompiler.require_file/2
    (elixir 1.10.4) lib/kernel/parallel_compiler.ex:306: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/mix_test.yml
  • actions/checkout v2
  • erlef/setup-beam v1
  • actions/checkout v2
  • erlef/setup-beam v1
  • ubuntu 20.04
  • ubuntu 20.04
mix
mix.exs
  • ex_doc ~> 0.23.0

  • Check this box to trigger a request for Renovate to run again on this repository

extra parentheses on a remote function capture

mix test spits compile warnings.

warning: extra parentheses on a remote function capture &DefinjectTest.Foo.quack()/0 have been deprecated. Please remove the parentheses: &DefinjectTest.Foo.quack/0
  test/definject_test.exs:26: DefinjectTest.Foo.bar/2

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.