GithubHelp home page GithubHelp logo

stephanos / rewire Goto Github PK

View Code? Open in Web Editor NEW
94.0 4.0 6.0 75 KB

Dependency injection for Elixir. Zero code changes required.

Home Page: https://hex.pm/packages/rewire

License: Apache License 2.0

Elixir 100.00%
elixir dependency-injection mocking

rewire's Introduction

rewire

Build Status Hex.pm

rewire is a dependency injection library.

It keeps your application code completely free from testing concerns.

And you can bring your own mock (mox is recommended).

Installation

Just add rewire to your list of dependencies in mix.exs:

def deps do
  [
    {:rewire, "~> 0.10", only: :test}
  ]
end

Usage

Given a module such as this:

# this module has a hard-wired dependency on the `English` module
defmodule Conversation do
  @punctuation "!"
  def start(), do: English.greet() <> @punctuation
end

If you define a mox mock EnglishMock you can rewire the dependency in your unit test:

defmodule MyTest do
  use ExUnit.Case, async: true
  import Rewire                                  # (1) activate `rewire`
  import Mox

  rewire Conversation, English: EnglishMock      # (2) rewire `English` to `EnglishMock`

  test "start/0" do
    stub(EnglishMock, :greet, fn -> "g'day" end)
    assert Conversation.start() == "g'day!"      # (3) test using the mock
  end
end

This example uses mox, but rewire is mocking library-agnostic.

You can use multiple rewires and multiple overrides:

  rewire Conversation, English: EnglishMock
  rewire OnlineConversation, Email: EmailMock, Chat: ChatMock

You can also give the alias a different name using as:

  rewire Conversation, English: EnglishMock, as: SmallTalk

Note that the rewire acts like an alias here in terms of scoping.

Alternatively, you can also limit the scope to a dedicated block:

  rewire Conversation, English: EnglishMock do   # (1) only rewired inside the block
    stub(EnglishMock, :greet, fn -> "g'day" end)
    assert Conversation.start() == "g'day!"      # (2) test using the mock
  end

Plus, you can also rewire module attributes.

FAQ

Will it work with async: true?

Yes! Instead of overriding the module globally - like meck - it creates a copy for each test.

Does it work with mox?

It works great with mox since rewire focuses on the injection and doesn't care about where the mock module comes from. rewire and mox are a great pair!

Will that slow down my tests?

Maybe just a little? Conclusive data from a larger code base isn't in yet.

Will test coverage be reported correctly?

Yes!

Will it work with stateful processes?

If the stateful process is started after its module has been rewired, it will work fine. However, if the module is started before - like a Phoenix controller - it won't work since it can't be rewired anymore. rewire is best used for unit tests.

Will it work with Erlang modules?

It is not able to rewire Erlang modules - but you can replace Erlang module references in Elixir modules.

How does it deal with nested modules?

Only the dependencies of the rewired module will be replaced. Any modules defined around the rewired module will be ignored. All references of the rewired module to them will be pointing to the original. You're always able to rewire them separately yourself.

How do I stop mix format from adding parentheses around rewire?

Add this to your .formatter.exs file:

import_deps: [:rewire]

Why do I need this?

I haven't been happy with the existing tradeoffs of injecting dependencies into Elixir modules that allows me to alter their behavior in my unit tests.

For example, if you don't use mox, the best approach known to me is to pass-in dependencies via a function's parameters:

defmodule Conversation do
  def start(mod \\ English), do: mod.greet()
end

The downsides to that approach are:

  1. Your application code is now littered with testing concerns.
  2. Navigation in your code editor doesn't work as well.
  3. Searches for usages of the module are more difficult.
  4. The compiler is not able to warn you in case greet/0 doesn't exist on the English module.

If you use mox for your mocking, there's a slightly better approach:

defmodule Conversation do
  def start(), do: english().greet()
  defp english(), do: Application.get(:myapp, :english, English)
end

In this approach we use the app's config to replace a module with a mox mock during testing. This is a little better in my opinion, but still comes with most of the disadvantages described above.

Witchcraft! How does this work??

Simply put, rewire will create a copy of the module to rewire under a new name, replacing all hard-coded module references that should be changed in the process. Plus, it rewrites the test code in the rewire block to use the generated module instead.

rewire's People

Contributors

danielefongo avatar kieraneglin avatar lexun avatar stephanos 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  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  avatar

rewire's Issues

Missing code coverage

One downside of running tests against a rewired copy of the actual module is that code coverage data is not available anymore. That's because Erlang's cover modifies tested modules by injecting custom instructions to track the coverage.

rewire should report the correct test coverage.

prior art: https://github.com/edgurgel/mimic/blob/c7529f8f9607d1ef0725dc723c5337531e5f8326/lib/mimic/cover.ex + https://github.com/eproxus/meck/blob/2c7ba603416e95401500d7e116c5a829cb558665/src/meck_proc.erl

rewire doesn't work through `use`

When rewiring a module in a test, we were noticing that it wasn't replacing all of the occurrences of a module. We eventually tracked down that the module in question was being brought in by a use macro. When traversing the AST, it appears that the use special form doesn't get expanded for rewire, so anything that brings in is left with the original module.

Is it possible to rewire deps with the same unscoped module name as module under test?

Given two modules:

  • App.Connector.Hubspot (used by app to interact with external resource)
  • App.Client.Hubspot (used by connector to make external calls)

It is not possible to (easily?) inject a mock for the client in the connector tests, since:

  • elixir does not support the syntax: rewire App.Connector.Hubspot, App.Client.Hubspot: HubspotMock
  • aliasing App.Client.Hubspot is ambiguous since the connector has the same module name

It seems plausible it could work with some alias tweaking, but this could be more easily solved by accepting a list of tuples instead of a keyword list, perhaps? Hopefully I can look more into this later.

Is it possible to rewrite modules using `alias Foo, as: Bar`?

Hey there! I found this library via this blog post and wanted to include it in my project.

One thing I've found is that it doesn't seem to support rewiring modules where the modules must us as:. Here's an example based on a real-world issue that I'm running up against during a refactor:

defmodule Foo do
  # See they have the same final name part so I _must_ use `as:`
  alias Foo.YtDlp.CommandRunner, as: YtDlpRunner
  alias Foo.Apprise.CommandRunner, as: AppriseRunner

  def yt_dlp_bar, do: YtDlpRunner.bar()
  def apprise_baz, do: AppriseRunner.baz()
  # ...
end

These are both modules that interface with CLI programs. Currently I'm using the standard Mox-recommended method of DI, but I dislike that it puts testing concerns in my main app code. I was hoping I could do something like this using rewire in my tests:

rewire Foo, YtDlpRunner: YtDlpRunnerMock, AppriseRunner: AppriseRunnerMock

# ...

But that doesn't seem to be possible. I could be missing something, but in my testing it seems like the AST is functionally rewritten to this:

defmodule Foo do
  alias Foo.YtDlp.CommandRunner, as: YtDlpRunnerMock
  alias Foo.Apprise.CommandRunner, as: AppriseRunnerMock

  def yt_dlp_bar, do: YtDlpRunnerMock.bar()
  def apprise_baz, do: AppriseRunnerMock.baz()
  # ...
end

Which only overwrites the as: alias, but it's still ultimately pointing to the original modules.


Is there a way to handle this and I just missed it? If not, would you be open to a PR that handles this case? This would be a breaking change but I think it's a valuable one

Rewire nested module support?

Thanks a lot for the library, it's a very cool idea to utilise aliasing and I really enjoy not having to deal with Application.fetch!

However, I am facing an issue with nested modules right now.

In my example,

defmodule Messaging do
  def send, do: Courier.send()
end

defmodule Courier do
  def send, do: Pigeon.send()
end

Pigeon is the mock that I want to implement, now, if I do:

defmock(PigeonMock, for: PigeonBehaviour)
rewire Messaging, Pigeon: PigeonMock

This compiler gives a nice helpful message and says it can't find Pigeon in Messaging, which is only available in Courier.

My actual use case is a little more complicated (with protocol dispatch), but this should illustrate the point just fine.

I can see that it is mentioned in the README,

Only the dependencies of the rewired module will be replaced. Any modules defined around the rewired module will be ignored. All references of the rewired module to them will be pointing to the original. You're always able to rewire them separately yourself.

But is this due to a technical reason, or will it be possible to do it? I'd gladly take a look at it if that's the case.

Since rewire works by replacing the module with a new version with alias, perhaps we could just recursively walk and replace the modules?

I haven't really looked into where it would work or not, but the proposed idea could look something like:

rewire Messaging, [Courier: [Pigeon: PigeonMock]]

This generates a new Messaging module that uses a rewired version of Courier, which uses a rewired version of Pigeon

Rewire doesn't work with cover

I've tried using rewire and test coverage but I always get an error on the output of what happened. The cover files get created, and the coverage gets reported, but the full execution fails.

Analysis includes data from imported files
[".../Elixir.CloudWatch.R8837-20673.coverdata"]
** (MatchError) no match of right hand side value: {:error, {:file, <<70, 79, 82, 49, 0, 0, 12, 196, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 1, 218, 0, 0, 0, 42, 16, 69, 108, 105, 120, 105, 114, 46, 83, 84, 83, 46, 82, 54, 48, 48, 48, 8, 95, 95, 105, 110, ...>>, :badarg}}
    (mix 1.15.0) lib/mix/tasks/test.coverage.ex:292: anonymous fn/3 in Mix.Tasks.Test.Coverage.html/2
    (elixir 1.15.0) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
    (mix 1.15.0) lib/mix/tasks/test.coverage.ex:291: Mix.Tasks.Test.Coverage.html/2
    (mix 1.15.0) lib/mix/tasks/test.ex:578: Mix.Tasks.Test.do_run/3
    (mix 1.15.0) lib/mix/task.ex:447: anonymous fn/3 in Mix.Task.run_task/5
    (mix 1.15.0) lib/mix/task.ex:502: Mix.Task.run_alias/6
    (mix 1.15.0) lib/mix/project.ex:458: Mix.Project.in_project/4
    (elixir 1.15.0) lib/file.ex:1624: File.cd!/2
    (mix 1.15.0) lib/mix/task.ex:604: anonymous fn/4 in Mix.Task.run_in_children_projects/2
    (elixir 1.15.0) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
    (mix 1.15.0) lib/mix/task.ex:603: Mix.Task.run_in_children_projects/2
    (mix 1.15.0) lib/mix/project_stack.ex:237: Mix.ProjectStack.recur/1
    (mix 1.15.0) lib/mix/cli.ex:92: Mix.CLI.run_task/2
    (elixir 1.15.0) lib/code.ex:1432: Code.require_file/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.