In many production systems you'll want to have one module capable of talking to many potential implementations of a collaborator module (e.g a in memory cache, a redis-based cache etc). While testing it's useful to control which module the module under test is talking to.
Here are the approaches I can see. The two points that seem to divide the approaches are their tool-ability (dialyzer) and their ability to handle stateful implementations (which need a pid).
Modules are first class, so you can pass them in. Used in EEx, where passed module must implement a behaviour.
defmodule Cache do
  use Behaviour
  defcallback cached?(any,any) :: boolean
  defcallback put(any,any) :: nil
end
defmodule Cache.Memory do
  def put(set,x) do: Set.add set, x
  def cached?(set,x) do: Set.member? map, x
end
defmodule Cache.Redis do
  def put(redis_pid,x) do 
    {:ok,1} = Redis.set redis_pid, x, 1
  end
  def cached?(redis_pid,x) do
    {:ok,x} = Redis.get(redis_pid,x)
    x != nil
  end
end
# usage
defmodule UsesCache do
  def start(cache,cache_pid) do
    cache.put(cache_pid,:hello)
    true = cache.cached?(cache_pid,:hello)
  end
end
UsesCache.start(Cache.Memory,HashSet.new)Similar idea to duck-typing.
- simple
 
- dializer(?)
 - modules with state - you'd have to pass a 
pidtoo, e.g{module,pid}(eugh) 
Write a Protocol for the functionality. You can then pass in an opaque value to collaborators, and the implementation will be decided at runtime.
- handles stateful and stateless implementations easily
 - dialyze-able
 
- requires stub implementations for testing
 
defprotocol Cache do
  def cached?(id,item)
  def put(id,item)
end
defmodule Cache.Memory do
  defstruct set: nil
  alias __MODULE__, as: Mod
  defimpl Cache, for: Mod do
    def put(%Mod{set: set},x) do: Set.add set, x
    def cached?(%Mod{set: set},x) do: Set.member? map, x
  end
end
defmodule Cache.Redis do
  defstruct redis: nil
  alias __MODULE__, as: Mod
  defimpl Cache, for: Mod do
    def put(%Mod{redis: redis},x) do
      {:ok,1} = Redis.set redis, x, 1
    end
    def cached?(%Mod{redis: redis},x) do
      {:ok,x} = Redis.get(redis,x)
      x != nil
    end
  end
end
# usage
defmodule UsesCache do
  def start(cache) do
    Cache.put(cache,:hello)
    true = Cache.cached?(cache,:hello)
  end
end
UsesCache.start(%CacheMemory{set:HashSet.new})For a single method, you could just pass a function. Then in tests you pass a stub method, and in production you can wrap up the real module behind it.
def start_link({some_callback}) do
  :gen_server.start_link(@name,SomeModule,{some_callback},[])
end
    
def init({some_callback}) do
  {:ok,%State{callback: some_callback})
endNow the callback field of state can be used by functions of this gen_server module.
# usage
defmodule UsesCache do
  def start(put,cached) do
    put.(:hello)
    true = cached.(:hello)
  end
end
# can create callbacks from anything: stateful, stateless etc- works with any implementation, even ad-hoc
 - dialyze-able
 
- pass many values for large APIs
 
Create a stateful module that holds the module, refer to that.
- doesn't work for use-cases with many instances (pass-the-pid)
 
- dialyzer - need a Behaviour for the return type of the getter
 
You could generate a module based on a run-time config.
- moving parts
 - doesn't work for use-cases with many instances (pass-the-pid)
 - dialyzer - seems likely to throw it off
 
You discuss a few approaches here, but have you found a solution that works for you yet?
As a person who's worked in large codebases and seen the benefits of both purity and isolated unit testing, I'm quite concerned about true isolation and DI and this sort of thing, as I explore Elixir.