Testing GenServer following PragDave's course

Since I watched the course of @pragdave, I had been following his pattern when it comes to coding GenServers.

I always separate the GenServer from the implementation.

For example:

defmodule LoginService.Auth do
  @moduledoc false
​
  alias LoginService.OAuth2
  alias LoginService.AuthState
​
  @doc """
  Returns the access token from the client.
  """
  @spec access_token(OAuth2.t()) :: String.t() | nil
  def access_token(%{token: nil}), do: nil
​
  def access_token(%{token: token}) do
    token.access_token
  end
​
  def fetch_access_token(client, timing \\ []) do
    case OAuth2.get_token(client) do
      {:ok, client} ->
        interval = normal_internal(timing)
        schedule_refresh(interval)
        # Notice that this client is from the return of the function, so it is a mutated client with some values on it
        client
​
      {:error, reason} ->
        interval = failed_internal(timing)
        schedule_refresh(interval)
        client
    end
  end
​
  defp normal_internal(timing) do
    Keyword.get(timing, :normal, :timer.minutes(55))
  end
​
  defp failed_internal(timing) do
    Keyword.get(timing, :failed, :timer.seconds(10))
  end
​
  defp schedule_refresh(interval) do
    Process.send_after(self(), :fetch_access_token, interval)
  end
end
defmodule LoginService.AuthServer do
  @moduledoc false
​
  use GenServer
  alias LoginService.Auth
​
  def start_link(args) do
    GenServer.start_link(__MODULE__, args, name: __MODULE__)
  end
​
  @doc """
  Returns the `access_token` of the client.
  """
  def access_token do
    GenServer.call(__MODULE__, :access_token)
  end
​
  @doc """
  Refresh the access token from the client.
  """
  def refresh_access_token do
    GenServer.cast(__MODULE__, :fetch_access_token)
  end
​
  @impl GenServer
  def init(client) do
    {:ok, client, {:continue, :fetch_access_token}}
  end
​
  @impl GenServer
  def handle_call(:access_token, _from, state) do
    access_token = Auth.access_token(state)
    {:reply, access_token, state}
  end
​
  @impl GenServer
  def handle_continue(:fetch_access_token, state) do
    {:noreply, Auth.fetch_access_token(state)}
  end
​
  @impl GenServer
  def handle_info(:fetch_access_token, state) do
    {:noreply, Auth.fetch_access_token(state)}
  end
​
  @impl GenServer
  def handle_cast(:fetch_access_token, state) do
    {:noreply, Auth.fetch_access_token(state)}
  end
end

Now, my struggle is when it comes to testing the GenServer.

  • Why should I test that module?
  • What test cases should I write?

I keep struggling in the tests due to the nature of a stateful server.

I keep putting timers to be able to test the internals of the GenServer like handle_cast.

I dislike my test cases.

However, I am not sure what else to do, or what should I test in particular for this module.

Should I test that the GenServer receive the message and that’s all?

The underline module is tested, so at this point, I am testing Elixir.

I don’t know, and I keep getting stuck in what is the right decision.

I can see how I could make mistakes around handle_cast, handle_call and the public API and stuff like that.

I would love to hear your thoughts around this and what decisions would you make about it.

Imho testing the internals of the genserver is not something you want to do. You want to test the public interface for the genserver against the sideeffects you expect. E.g. in your case if you do:

test "refreshing does change the access token" do
  start_supervised!(LoginService.AuthServer)
  let old_access_token = LoginService.AuthServer.access_token()
  LoginService.AuthServer.refresh_access_token()
  assert old_access_token != LoginService.AuthServer.access_token()
end

As messages between two processes are guaranteed to be received in order on the receiving end you can be sure the cast will be handled before the second call into your genserver.

Generally you genserver doesn’t really do very much, though, so I’m not sure what else you should want to test here. Maybe "it starts with an access token available", …

4 Likes

Isn’t it tested by the tests for the API?

Sorry Dave, I didn’t understand your question. Could you elaborate a bit more?