[erlang-questions] Mocks and explicit contracts
Fred Hebert
mononcqc@REDACTED
Thu Nov 19 16:53:06 CET 2015
On 11/19, Roger Lipscombe wrote:
>In http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/,
>José Valim suggests a way to use dependency injection to more easily
>unit test Elixir code.
>
>However, it doesn't translate to Erlang...
>
>In one example, he stores the dependency (as a module) in the
>application environment and then defines a private function to
>retrieve it:
>
> defp twitter_api do
> Application.get_env(:my_app, :twitter_api)
> end
>
>Then it can be called as:
>
> twitter_api.get_username(username)
>
Dear god is that a possibility? I'm pretty annoyed to begin with, at the
prospect of a seeming module name. I figure that's the Erlanger in me
being confused by Elixir using Uppercased module names and lowercased
function/variable names there.
The insidious thing about this practice is that every function call you
make to the twitter API now includes:
- an access to a shared ETS table, acquiring a read lock
- extracting the value from ETS and copying it to your process
- an access to a shared ETS table, releasing a read lock
- a lookup to make a dynamic fully-qualified function call
The interesting thing then, is that how fast you can call your function
in production now depends on how many concurrent calls exist and how
much flexibility was desired in tests. That's not great.
>- Is Jose's approach applicable to Erlang at all?
>- Are there idiomatic ways to do something like this in Erlang?
>
>I know about (and use) 'meck', which is awesome, but has the
>disadvantage that it mocks (verb) globally, which limits running tests
>in parallel. It can also be quite slow, because it compiles the mocks
>on the fly. José's approach uses mocks (noun) locally.
First of all, José's approach also mocks globally in the Erlang context.
It might be different using local modules, but if you enter Erlang, then
it's automatically global.
Would you consider, for that case, using instead:
-ifndef(twitter_api).
-define(twitter_api, default_twitter_api).
-endif.
show(Username) ->
?twitter_api:get_username(Username).
Through build tools, you could define (here I'm using rebar3):
{profiles, [
[{test, [{erl_opts, [{d, twitter_api, test_twitter_api}]}]}]
]}.
You now get the flexibility you want, without mocks, without global data
(although it is globally mocked), and without runtime cost, at the added
visual overhead of a single '?' at the call-site.
Then again, I'm not a super big fan of doing that kind of stuff. In the
end, the questions I have to ask is:
Q1: Am I going to need multiple modules there because it's a value I
want to be configurable by users of a library at runtime?
A1: if so, then application environment is not the right way, unless I
plan on users only being able to configure one thing. If more than one
library or OTP app relies on my library, they have to agree on the
implementation. That sucks.
The best way to do this form is to configure the module explicitly when
starting the processes, and have it carry it around in state. This is,
essentially, the safest way to concurrently allow multiple
configurations at once.
Q2: Am I needing multiple modules to test alternating configurations?
A2: If so, consider the answer A1. If this is considered costly and the
configuration is mostly static (i.e. windows vs. linux yielding
different results), then consider the macros mentioned above.
Q3: Am I needing to swap an implementation to make testing easier?
A3: If so, use mocking. It requires no code change, and it's a much
better idea (to me, anyway) to slow down test execution a bit than
slowing down runtime execution because of my testing requirements. Talk
about side-effects!
If really mocking is unpractical, consider the macros.
If someone sent me that 'globally configurable through app env dynamic
module calls' in a code review, it would not make it to production. I
would ask for this to be reviewed, because I think it's a lazy shortcut
that has too many unintended consequences to be acceptable.
I don't care for the syntax, it's just bad semantics, runtime
properties, etc.
Here's a short experiment:
-module(mod).
-compile(export_all).
f(M) ->
{timer:tc(?MODULE, ncalls, [100000, fun() -> mod:g() end]),
timer:tc(?MODULE, ncalls, [100000, fun() -> M:g() end]),
timer:tc(?MODULE, ncalls, [100000, fun() -> (application:get_env(app, val, M)):g() end])}.
g() -> ok.
ncalls(0, _) -> ok;
ncalls(N, F) -> F(), ncalls(N-1, F).
What's the runtime results for 100,000 calls?
- mod:g(): 5827 µs
- M:g(): 7326 µs
- (application:get_env(app, val, M)):g(): 30770 µs
The solution chosen if the last one is taken is therefore agreeing that
every one of your function calls to that API in production are going to
be roughly 5x slower, *because* you wanted tests to run faster. It will
also require code changes and variations that are otherwise not required
with mocks. The cost of that application:get_env would be the same in
Elixir I figure.
It's just an inadequate solution to a problem that has been solved
better, multiple times over. I personally prefer to pay the cost of
slightly longer test runs personally, than having my users pay the cost
themselves at run time.
Regards,
Fred.
More information about the erlang-questions
mailing list