[erlang-questions] Mocking in Erlang

Christian S chsu79@REDACTED
Tue Jun 3 18:19:05 CEST 2008


On Tue, Jun 3, 2008 at 2:14 PM, Dominic Williams
<erlang@REDACTED> wrote:
> - invert the dependency: avoid side-effects and don't call out to
>  ibrowse directly from the module that needs to be tested: have
>  it return lists of "ibrowse command" terms, and let the caller
>  pass these on to a dumb (untested) module that makes the actual
>  ibrowse calls given these terms

I have some thoughts spinning in my mind about dependency injection in
Erlang. This became rather long:

In Java dependency injection is a quite materalised/working idea. It
works like this; rather than creating instances of the explicitly
named classes you need, you instead refer to the classes by an
interface, and you expect to have the interface implementing objects
passed to you in your constructor.

So what is done is that you have a "Location", a "Primate" and a
"Consumable" interface. You have a separate specification that maps
implementing classes to interfaces: Jungle implements Location,
Gorilla implements Primate, Banana implements Consumable.

Now, this buys something (at the cost of making an already very overly
verbose Java even more verbose) and that is the ability to supply Mock
objects. One can create a Gorilla and pass it your mock objects in the
constructor: a Beer implements Consumable, and a Cage implements
Location. This provides you with a setting for safe testing of the
Gorilla under known conditions; having the Consume object throw weird
exceptions in the consume method. Maybe finding out that the gorilla
itself throws a "weird exception" after consuming a few beers.

I have also observed how it leads to better designs in themselfes,
like you have Dominic:

This decoupling also makes it easier to add future features. Just
imagine version 2008 where you have City implements Location, Human
implements Primate and Latte implements Consumable. You can get there
in small increments, changing one interface implementation a time.
(Perhaps by testing how your Primate likes Beer :).

==Back to Erlang==

=processes=

We don't need interfaces for Erlang processes the way Java does, since
process protocols are (still?) only informal, you can send them
whatever message you want. So that is easier for us than Java.

However, we can suffer from hard-coding the function used in spawning
a process. If our gorilla process has a spawn_link(fun banana:start()
end) for the Consumable, then it is going to be difficult to have it
use our mock Consumable.

One can prepare for testable code, such as :
* having the Module:init(Args) method in gen_servers look for an
alternative fun used when spawning and store that fun in the State
parameter, but fall back to (fun banana:start() end).
* depend on (the still unofficial, seemingly staying so) parameterized
modules to supply overriding module names when testing
* if possible, avoid creating processes and having them be sent in
spawn arguments or in messages

Either way adds some "cruft" to code, but why expect that the
additional requirement of testability should come without any cost?

=external calls=

This is a bit trickier.  It is the problem where you want to mock
ibrowse like the example earlier in the thread.

What is bad is the need to explicitly compile for testing. Eunit's
separate-compile-for-testing is actually bad, but I see why it is
required there. Lets try to avoid it.

Is there any input from dialyzer people and compiler optimization
people on how you would prefer having a mock implementation of ibrowse
injected in a module without recompiling it?

Lets say one would want a few testcases: tests that guarantees that an
ibrowse request always signal various remote status codes (404 not
found, 501 internal error,...), and not having an actual request go
out through ibrowse and externally to a simulating web server. Such a
fixture is too expensive to set up if your goal is to have 10000 tests
like it, tests that a programmer can run in a minute to assure himself
that things are still working before a commit.

> - use parameterised modules, with the name of the module to be
>  used to make ibrowse calls being a parameter.

This is quite attractive, I much prefer this to the "oo-surrogate"
people try to use parameterized modules for. Although, again,
parameterized modules are still an unofficial feature.

One can have

-module(spider_modules).
spider_server() ->
  spider_server:new(ibrowse,...).

-module(spider_server, [IBrowse, ...]).
handle_call({spider, URL}, _, S0) ->
  ...,
  Data = IBrowse:request(URL),
  ...,
  {ok, Report, S1}.

Then always start with gen_server:start_link(Modules:spider_server(),
Args, []) in code.

Allowing one to supply another value for Module during testing.

How does one maximize usefulness from dialyzer here? I want the code
analyzed as if IBrowse has the value ibrowse.

==summary==

Erlang is in a good position for writing testable code. There is just
a lack of focus and best practices for it. There is more than one way
to do it. Right now it takes experience rather than "book smart" to
know how.



More information about the erlang-questions mailing list