testing erlang gen_server with gen_server_mock

Testing by synchronous pattern matching

Testing multi-process erlang gen_servers can be tricky. Typically one relies simply on pattern matching to verify that the response matches what you would expect.

{expected, Response} = gen_server:call(Pid, hi).

As long as the gen_server call hi returns expected as the first element of the tuple, then the tests pass.

The technique is also the same when building client-server code where both client and server are gen_servers. The common case is to simply test one side at a time; test the response of all client calls and then (independently) test the responses of the server calls.

What about asynchronous cast?

gen_server:call is convenient because it is synchronous and returns a value.
gen_server:cast, on the other hand, is asynchronous and always returns the atom ok. This can make casts difficult to test.

gen_server_mock is a library to mock gen_server processes that expect specific, ordered sets of messages. It allows you to unit test gen_servers by verifying they are receiving the expected set of messages.

Example 1

     {ok, Mock} = gen_server_mock:new(),
     gen_server_mock:expect(Mock, call, fun({foo, hi}, _From, _State) -> ok end),
     gen_server_mock:expect_call(Mock, fun({bar, bye}, _From, _State) -> ok end),
 
     ok = gen_server:call(Mock, {foo, hi}),  
     ok = gen_server:call(Mock, {bar, bye}),  
 
     ok = gen_server_mock:assert_expectations(Mock)

This Mock expects two calls: {foo, hi} and {bar, bye}. Since Mock receives both of these messages, assert_expectations does not raise any errors.

What is verified

gen_server_mock:assert_expectations(Mock) verifies that:

  1. all expected messages were received
  2. no messages were received that were not expected

You can catch the exit by using the following:

     Result = try gen_server_mock:assert_expectations(Mock)
     catch
         exit:Exception -> Exception
     end,
     % etc...

Example 2

     {ok, Mock} = gen_server_mock:new(),
 
     gen_server_mock:expect_call(Mock, fun(one,  _From, _State)            -> ok end),
     gen_server_mock:expect_call(Mock, fun(two,  _From,  State)            -> {ok, State} end),
     gen_server_mock:expect_call(Mock, fun(three, _From,  State)           -> {ok, good, State} end),
     gen_server_mock:expect_call(Mock, fun({echo, Response}, _From, State) -> {ok, Response, State} end),
     gen_server_mock:expect_cast(Mock, fun(fish, State) -> {ok, State} end),
     gen_server_mock:expect_info(Mock, fun(cat,  State) -> {ok, State} end),
 
     ok = gen_server:call(Mock, one),
     ok = gen_server:call(Mock, two),
     good = gen_server:call(Mock, three),
     tree = gen_server:call(Mock, {echo, tree}),
     ok = gen_server:cast(Mock, fish),
     Mock ! cat,
 
     gen_server_mock:assert_expectations(Mock)

Currently three types of messages are supported: call, cast, and info.

The signature of the fun of each expectation is the same as the corresponding
gen_server:handle_*. So the fun for expect_call has the same signature as handle_call: fun(Request, From, State). See man gen_server for more information.

However, the return value of the fun must be one of:

    ok |                  
    {ok, NewState} |
    {ok, ResponseValue, NewState} |

Anything else will be an error. Note that you can change the state of your Mock by returning NewState.

Arbitrary, non-gen_server messages are handled with expect_info, e.g. Mock ! cat fulfills the expect_info in the example above.

References

Share:
  • del.icio.us
  • Reddit
  • Technorati
  • Twitter
  • Facebook
  • Google Bookmarks
  • HackerNews
  • PDF
  • RSS
This entry was posted in programming. Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.