fireproofsocks

fireproofsocks

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).

Most Liked

ityonemo

ityonemo

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

dimitarvp

dimitarvp

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.

ityonemo

ityonemo

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.

Where Next?

Popular in Discussions Top

Donovan
Hello everyone, I’m so glad to have discovered this awesome community. Thanks for creating it! This is my second post, and apologies for...
New
nunobernardes99
Hi there Elixir friends :vulcan_salute: In a recent task I was on, I needed to check in two dates which of them is the maximum and which...
New
AlexMcConnell
The reason that Rails is as popular as it is is because it’s very easy for relatively inexperienced developers to get a lot of work done....
588 19652 166
New
Crowdhailer
I’ve been hearing much about the new formatter and it’s something I have been keen to try. I find examples buy far the most illuminating...
248 19327 150
New
mbenatti
Following https://github.com/tbrand/which_is_the_fastest |> https://raw.githubusercontent.com/tbrand/which_is_the_fastest/master/imgs...
New
rower687
Hi all, I’ve been reading a lot about the “let it crash” term and how supervising processes and the whole messaging passing make an elixi...
New
rms.mrcs
A couple of days ago I was discussing with a friend about different approaches to write microservices. He said that if he was going to w...
New
joeerl
I’m playing with Elixir - It’s fun. I think @rvirding does give Elixir courses these days. Re: files and database - when I given Erlang ...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
New

Other popular topics Top

New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43487 311
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52673 488
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement