4

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?

vfsoraki
  • 2,186
  • 1
  • 20
  • 45
  • What would you like to test in `handle_cast` here? That it was able to initial contact with the external resource? or that it also succeeded? Do you want to actually contact that external resource during every test run? (Some rough outline of the code would also be helpful.) – Dogbert Dec 07 '16 at 07:16
  • No, I inject the external resource interface module into GenServer, so I don't want to actually contact the resource (it would cost me money). Since `handle_cast` uses some functions of the interface module, I want to test that it succeeds when the functions return okayish values. Adding some code is a good idea, I will do it today, in some hours. – vfsoraki Dec 07 '16 at 08:29

2 Answers2

5

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.

s3cur3
  • 2,749
  • 2
  • 27
  • 42
michalmuskala
  • 11,028
  • 2
  • 36
  • 47
  • Good points, 1 & 2. But 3 is a bit unclear, how would one test some effects that are being done in another process? Seems there is no general way for these cases, as each case may or may not have side effects. Even those having, may have basically different ones so assertions would be different. – vfsoraki Dec 07 '16 at 08:32
2

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).
Pascal
  • 13,977
  • 2
  • 24
  • 32
  • Seems the right solution in my case, where my interface functions just act as delegates to `handle_*` functions. IDK about other use cases with more complex interface functions, but this will solve my problem. Thanks! – vfsoraki Dec 07 '16 at 10:52