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
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
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
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.
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









