Elixir Service Locator (Service Container) : Mocking 3rd party APIs and more

I’m sharing a pattern I’ve been tooling with. More or less, it’s an Elixir implementation of a Service Locator pattern (which is dubbed as an anti-pattern by some, but I feel like the realities of functional programming make some of the normal drawbacks less severe and overall a net positive).

This pattern is used thoroughly by PHP’s Laravel framework (and others), and is encapsulated quite cleanly in the PHP Pimple Package (yes, that’s an awful name). I confess that I personally never liked Python or Ruby’s monkey-patching approach to testing: I much prefer the explicit elegance of dependency injection that I think PHP does surprisingly well.

In Elixir, this pattern can be implemented without much fuss by using Application.get_env/2 and Application.put_env/3. Although it’s not strictly necessary, the syntax of this pattern is much more pleasing if you have a convenience function like the following defined for your app:

def app(key), do: Application.get_env(:my_app, key)

(The function name app here I’m borrowing directly from Laravel’s app function – but choose whatever name you want).

And then, in your config, you can define your “service” modules:

# config.exs
import Config

config :my_app,
  json_module: Jason,
  http_client: HTTPoison,
  stripe_client: StripeModule
  # ... etc ...

This allows us to elegantly call our configured modules, e.g.

# assume: `import MyApp` 

app(:http_client).get("http://some-url.com")

AND we can override the modules with mocks for testing by using Application.put_env/3.

For example, if I wanted to test what happens if my HTTP get request returns a 404, I can mock this in a module. It helps if I configure my mix.exs

def project do
    [
    elixirc_paths: elixirc_paths(Mix.env()),
    # ... etc....
    ]
end

defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

then I can add my mocks inside the test/support/ directory, e.g.

defmodule MyApp.Mocks.HttpClient404
    def get(url) do
       {:error, "Probably this is more believable if you actually pasted in an actual error from a REAL 404 response"}
    end
end

Then my test can do something like:

# Use a setup block with `on_exit` to ensure all configured modules are put back into place after each test
setup do
  http_client = Application.get_env(:my_app, :http_client)

  on_exit(fn ->
    Application.put_env(:my_app, :http_client, http_client)
  end)
end

test "invalid URLs generate 404" do
    Application.put_env(:my_app, :http_client, MyApp.Mocks.HttpClient404)
    assert {:error, _} = MyApp.do_something_that_calls_http_client()
end

Hopefully that makes sense. This doesn’t seem like a perfect solution, but it doesn’t feel completely smelly either: it lets me test all aspects of 3rd party services going bezerk, and that’s a big win. The syntax smoothing is a nice bonus too.

Hopefully this is helpful to someone (and I sincerely hope I’m not pedaling nasty anti-patterns here).

This is a fairly standard practice in basically all commercial Elixir projects I participated in. Writing Application.get_env can get tedious even with IDE autocompletion, plus some projects want to supply config through other means.

Thus, having something like a MyApp.RuntimeConfig module with get and put functions is being used extensively. I usually alias such a module to Cfg and my code using the runtime config gets terse while still being quite easy to reason about.

1 Like

I’m glad to hear that others are using this pattern. Based on the feedback I’ve gotten in the forums and from numerous Github issues across multiple Elixir projects (usually along the lines of “why would you want to test that?”), it has felt to me like this strategy isn’t as commonplace as it could be. I suspect the !#%! up way Python and Ruby deal with monkey-patching methods + shunning dependency injection + not relying on service containers maybe has influenced the way people test (or don’t test) their apps, but that’s just my opinionated guess.

This is a bit of a bad pattern. Application envs are global, so if you have concurrently running tests you will have all sorts of race conditions while running your tests. You should use Mox instead, which can manage a table of dependency injections so that you can sanely do mocks in concurrently running tests. I call it the Multiverse pattern; each test runs in its own universe with it’s own view of the system state (in this case dependencies) and Mox controls the boundaries between the universes of the multiverse.

1 Like

Mox? This? https://hexdocs.pm/mox/Mox.html ? How is that different than using a service locator and running tests synchronously?

you can run them asynchronously.

  1. it results in your tests completing faster
  2. it stresses your system out and uncovers race conditions, concurrency errors and hidden dangerous statefulness faster.

I run a highly concurrent system in prod and the fact that I have hundreds of unit tests and integration tests that get run on each pull request to the devel branch (and master) helps me sleep at night.

1 Like

Ok, but just to be clear, this has nothing to do with Mox.

well the fact that they are concurrent does. I would not be as confident about a concurrent system if my tests were all synchronous. To be fair mox isn’t the only concurrent tool I have in my tests. Ecto, as well, has concurrent testing, and I have a few homebrewed things (that I’ll probably release as open source someday) that let me shard Registries and Phoenix.PubSub

2 Likes

Can you recommend any public repos that use Mox that would be good to look at? I’m having a hard time following its documentation.

http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/

here’s a bit of a demo to show how mox works with tasks:

the key points are contained in a single file:

I just looked through all of my public repos, and none of them use mox, which kind of makes sense, because they’re all bits and pieces, none of them are full systems, and so none of them has a contract boundary that’s worth creating a mock for.

1 Like