[erlang-questions] Why does adding an io:format call make my accept socket remain valid?

zxq9 zxq9@REDACTED
Mon Mar 28 04:13:03 CEST 2016


On 2016年3月27日 日曜日 21:34:16 Matthew Shapiro wrote:
> Why is that?  I was under the impression that one of the advantages was
> that you can test the handle call/cast/info functions without having to
> stand up a full server to do a small surface area unit test.

You could do test the callbacks in isolation, but the callbacks themselves should be so tiny that they don't mean anything without the OTP machinery.

Most of the time you shouldn't be putting a lot of logic into your handle_* function clauses. You want the handle_* functions to be doing just one thing: handling the message. Plagiarizing from a marginally related post I wrote last week:

  spawn_mob(Options) ->
      gen_server:cast(?MODULE, {spawn_mob, Options}).

  %...

  handle_cast({spawn_mob, Options}, State) ->
      ok = do_spawn_mob(Options, State),
      {noreply, State};

  % ...

  do_spawn_mob(Options, State = #s{stuff = Stuff}) ->
      % Actually do work in the `do_*` functions down here
      % Side effects go here,
      OtherStuff = some_pure_function(Stuff)
      the_target_effect(OtherStuff).


You do NOT want this:


  handle_cast({spawn_mob, Options}, State = #s{stuff = Stuff}) ->
      % A few lines of stuff
      % Shoving everything into *radically* different clauses
      % across your handle_*, making it a constantly morphing
      % and difficult to navigate structure of its own over time.
      OtherStuff = select_something(fun(X) -> Continue = case check_mob(X, State) of
                                                 good ->    % WHY DO PEOPLE
                                                     ok;    % DO THIS TO
                                                 bad ->     % THEMSELVES?!?
                                                     reset(X)
                                    end, Options, Stuff),  
      ok = the_target_effect(OtherStuff),
      {noreply, State};
  handle_cast({register, Player}, State) ->
      % A bunch of lines of details completely unrelated to
      % mob spawning.
  ...


Why this breakdown? Because in the first case we have broken the concerns of each function down into separate elements, concentrated the side effects in a single spot (the do_* parts), can now test each pure function on its own, know for sure what has a side effect that needs to be tested in the context of a more complete environment, and have the handle_* functions doing exactly one thing: dispatching based on received messages.

This way is testable. The other way isn't.

A few quick hacks with long fun definitions in argument lists which themselves contain cases or other funs within the the handle_* clauses is all it takes to turn an easily understandable and maintainable gen_server into the kind of code that actually makes you *feel* bad when you open it to make an edit.

As for testing...

[Digression ahead! Heresy warning!]

I occasionally go as far as to write a test/0 that returns ok or crashes when called, and all it does is call tests for each pure function in the module -- just the pure functions. This gives me a motivation to isolate side effects, and prevents the case I see so often in older commercial products where the a test/ or tests/ directory is chock full of test modules that don't really describe the code at all anymore, and nobody has the time to go pick through there and sort out what is useful, what is dead code and what tests are just flat out useless. By including tests only for pure functions within the module itself I know they are tests of value, not effect, and that they should be written to define clear bounds to the function and all the other strict things you can do with a pure function. Outside of that I can also know that everything in a test/ directory should test the *module* as it would exist within a running system, to include the OTP stuff above it, since that is part of the process the module represents.

On that note, testing beyond the module can usually only tell you a little bit of what you want to know about the system running as a whole in production. I've found that integration testing is generally not as useful as actual user testing once the big pieces reach that "works for me" stage throughout the dev team. But this means you have to stand up an actual (gasp!) test infrastructure that is identical to production and stands parallel to it. Which most folks seem to not have the patience for ("We have tests, I don't see how that buys us much" and so on), and requires that you are prepared for people to do things in your system you didn't expect (which was going to be the case once its live anyway, so wtf?!?).

[/digression /heresy]

-Craig


More information about the erlang-questions mailing list