Skip to content

Instantly share code, notes, and snippets.

@driv3r
Last active June 26, 2018 18:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save driv3r/03f4b4b8fac13444fac41503bbff86d7 to your computer and use it in GitHub Desktop.
Save driv3r/03f4b4b8fac13444fac41503bbff86d7 to your computer and use it in GitHub Desktop.
Elixir design and testing patterns

Setup

Application consist of 3 servers:

  • Queue, for fetching/deleting messages from the queue
  • Token, for fetching access token required for other service
  • Cron, for republishing messages from queue to 3rd party service by using access token authentication

Each of the HTTPoison calls are supposed to be replaced by proper Endpoints which could have used @behaviour and have an Http implementation, that could be set via application configuration and tested with mox.

Issue

We assume that other servers/apis are tested, how do we now test the server that manages all other servers/apis?

  1. We could define here in test module dummy module mocks for each service and each case and pass them along. Issues:
    • dummy modules can get out of sync easily
    • dummy module per mock result
  2. We could specify for each server client a set of @callbacks and then use mox for creating mocks. Issues:
    • Many useless behaviours which will be there only for test purposes
  3. We could pass in functions instead of modules, i.e. cron(fetch: &state.queue.fetch_message/0, delete: &state.queue.delete_message/1, etc..), then in the test we can just pass in different functions, instead of whole modules. Issues:
    • isn't it too verbose?
  4. Any other ways?
defmodule CronServerState do
defstruct [:fetch, :update, :delete, retries]
@type msg() :: String.t()
@type resp() :: HTTPoison.Response.t()
@type t() :: %__MODULE__{
retries: non_neg_integer(),
fetch: (() -> resp()),
update: (msg() -> resp()),
delete: (msg() -> resp())
}
def new(config) do
queue = Keyword.get(config, :queue, Queue)
token = Keyword.get(config, :token, Token)
url = Keyword.get(config, :url)
%__MODULE__{
retries: Keyword.get(config, :retries, 0),
fetch: &queue.fetch_message/0,
update: fn(msg) -> HTTPoison.post!(url, msg, [{"Authorization", token.fetch_token}]) end),
delete: &queue.delete_message/1
}
end
end
defmodule CronServerStateTest do
use ExUnit.Case, async: true
describe "new/1" do
# somehow assert defined functions?
end
end
defmodule CronServer do
use GenServer
alias HTTPoison.Response
def start_link(opts) do
GenServer.start_link(
__MODULE__,
:app |> Application.get_env(:cron_server) |> CronServerState.new(),
opts
)
end
def init(state) do
schedule()
{:ok, state}
end
def handle_info(:cron, state) do
retries = cosume(state)
schedule()
{:noreply, %{state | retries: retries}}
end
def cron(state) do
case state.fetch.() do
%Response{status_code: 200, body: body} ->
state.update.(body)
state.delete.(body)
0
%Response{status_code: 404} ->
0 # no messages
_ ->
state.retries + 1 #report error
end
end
defp schedule, do: Process.send_after(self(), :cron, 500)
end
defmodule CronServerV1Test do
use ExUnit.Case, async: true
alias CronServer, as: Serv
alias CronServerState, as: State
describe "cron/1" do
# no more mocks, pass in the functions directly as part of the state
test "increments retries when error" do
state = %State{pull: fn -> HTTPoison.Error{} end, retries: 5}
assert Serv.cron(state) == 6
end
end
end
defmodule Queue do
def fetch_message(server \\ __MODULE__), do: GenServer.call(server, :fetch_message)
def delete_message(message, server \\ __MODULE__), do: GenServer.call(server, {:delete_message, message})
end
defmodule Queue.Server do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, %{url: Application.get_env(:app, :queue_url)}, opts)
def init(state), do: {:ok, state}
def handle_call(:fetch_message, _, state), do: {:reply, HTTPoison.get!(state.url), state}
def handle_call({:delete_message, message}, _, state), do: {:reply, HTTPoison.delete!(state.url, message), state}
end
defmodule Token do
def fetch_token(server \\ __MODULE__), do: GenServer.call(server, :fetch_token)
end
defmodule Token.Server do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, %{url: Application.get_env(:app, :token_url)}, opts)
def init(state), do: {:ok, state}
def handle_call(:fetch_token, _, state), do: {:reply, HTTPoison.get(state.url).body, state}
end
@driv3r
Copy link
Author

driv3r commented Jun 26, 2018

so far so good, 4th iteration looks more reasonable, still wondering how to test functions setup

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment