[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