Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test handle_cast in a GenServer properly?

I have a GenServer, which is responsible for contacting an external resource. The result of calling external resource is not important, ever failures from time to time is acceptable, so using handle_cast seems appropriate for other parts of code. I do have an interface-like module for that external resource, and I'm using one GenServer to access the resource. So far so good.

But when I tried to write test for this gen_server, I couldn't figure out how to test the handle_cast. I have interface functions for GenServer, and I tried to test those ones, but they always return :ok, except when GenServer is not running. I could not test that.

I changed the code a bit. I abstracted the code in handle_cast into another function, and created a similar handle_call callback. Then I could test handle_call easily, but that was kind of a hack.

I would like to know how people generally test async code, like that. Was my approach correct, or acceptable? If not, what to do then?

like image 845
vfsoraki Avatar asked Mar 10 '23 11:03

vfsoraki


2 Answers

The trick is to remember that a GenServer process handles messages one by one sequentially. This means we can make sure the process received and handled a message, by making sure it handled a message we sent later. This in turn means we can change any async operation into a synchronous one, by following it with a synchronisation message, for example some call.

The test scenario would look like this:

  1. Issue asynchronous request.
  2. Issue synchronous request and wait for the result.
  3. Assert on the effects of the asynchronous request.

If the server doesn't have any suitable function for synchronisation, you can consider using :sys.get_state/2 - a call meant for debugging purposes, that is properly handled by all special processes (including GenServer) and is, what's probably the most important thing, synchronous. I'd consider it perfectly valid to use it for testing.

You can read more about other useful functions from the :sys module in GenServer documentation on debugging.

like image 132
michalmuskala Avatar answered Mar 19 '23 12:03

michalmuskala


A cast request is of the form:

Module:handle_cast(Request, State) -> Result

Types:
Request = term()
State = term()
Result = {noreply,NewState} | 
         {noreply,NewState,Timeout} | 
         {noreply,NewState,hibernate} |
         {stop,Reason,NewState}
NewState = term()
Timeout = int()>=0 | infinity 
Reason = term()

so it is quite easy to perform unit test just calling it directly (no need to even start a server), providing a Request and a State, and asserting the returned Result. Of course it may also have some side effects (like writing in an ets table, modifying the process dictionary...) so you will need to initialize those resources before, and check the effect after the assert.

For example:

test_add() ->
    {noreply,15} = my_counter:handle_cast({add,5},10).
like image 21
Pascal Avatar answered Mar 19 '23 13:03

Pascal