unit-testingelixirerlang-otpex-unit

How to test GenServer restart behaviour?


In my app I have a GenServer. It backs up data needed to start again in an Agent. I want to test if my GenServer backs up and restores correctly, so I wanted to start backup agent, then restart GenServer and see if it works (remembers config from before restart).

Right now I have GenServer configured and started (with start_supervised!) in test setup. I need to somehow restart that GenServer.

Is there a good way to do it? Should I be doing it completely differently? Is there a different, correct way of testing restart behavior?


Solution

  • A Supervisor decides when to restart a process under its supervision through the child_spec of that child process. By default, when you define your GenServer module, and use use GenServer on the module, the default (restart values) will be :permanent, which means, always restart this process if it exits.

    Given this, it should be enough to send it an exit signal, with Process.exit(your_gen_server_pid, :kill) (:kill will ensure that even if the process is trapping exits it will be killed), and the supervisor should then start the process again and you can then do your assertions.

    You'll need a way to address the "new" genserver process, since it will be killed, when restarted its pid won't be the same as was originally, usually you do that by providing a name when starting it.

    If your genserver loads the state as part of its init you don't necessarily need to supervise it to test the backup behaviour, you could just start it individually, kill it, and then start it again.

    There might be edge-cases depending on how you establish the backup, etc, but normally that would be enough.

    UPDATE:

    To address both the process exiting and being up again, you could write 2 helper functions to deal specifically with that.

    def ensure_exited(pid, timeout \\ 1_000) do
      true = Process.alive?(pid)
      ref = Process.monitor(pid)
      Process.exit(pid, :kill)
      receive do
         {:DOWN, ^ref, :process, ^pid, _reason} -> :ok
      after
        timeout -> :timeout
      end
    end
    

    You could make it take instead a name and do GenServer.whereis to retrieve the pid, but the idea is the same.

    To make sure it's alive:

    def is_back_up?(name, max \\ 200, tries \\ 0) when tries <= max do
        case GenServer.whereis(name) do
           nil ->
             Process.sleep(5)
             is_back_up?(name, max, tries + 1)
           pid -> true
        end
    end
    
    def is_back_up?(_, _, _), do: false
    

    The basic idea is that. Not sure if there's already some helpers to do this sort of thing.

    Then you just use that (you could write a 3rd helper that takes the live pid, the name, and does it all in one "step"), or write:

    :ok = ensure_exited(pid)
    true = is_back_up?(name)