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.