Manzanit0
Designing an Agent with side-effects on start_link
Hi all!
Some time ago I started to work on an application and I designed one of it’s workers with a start_link/0 such as this:
def start_link do
Logger.info("Starting worker")
status = @my_service.get_api_response!()
Agent.start_link(fn -> status end, name: __MODULE__)
end
I wasn’t too worried because the business logic didn’t have any side-effects apart from that start_link and the workaround I found was to simply run mix test witht the --no-start flag while starting up all the necessary applicaitions in the test_helper.ex.
The issue I’ve encountered is that when trying to adopt Phoenix now, it’s no longer convenient to run mix start --no-start, so reality kinda called back and made me deal with the real issue: How would I design an Agent/Genserver which needs to pull its initial status from an external HTTP API?
Most Liked
peerreynders
How would you test that?
Following dependency rejection you would design the system to obtain the initial state before even attempting to start the GenServer - then there is no dependency that needs to be mocked out because the initial state is handed in as a plain value.
danj
Two things come to mind:
Unless you want the whole application to fail at startup if the initial state request fails, issue an Agent.cast after the start_link to initialize the Agent. Let the cast handler do the init. Since that cast will be first in the message queue, nothing will get to the Agent before it’s initialized. Also, in the init, you can use an Application env to control if it pulls that initial state. While you can’t use Mix.env, you can use a config that’s dependent on the Mix.env that’s set up at compile/release.
LostKobrakai
How about just not testing the genserver started by your supervision tree, but starting up additional ones with custom configuration just for your tests:
setup do
{:ok, pid} = start_supervised({MyApp.TheWorker, mock_config()})
{:ok, pid: pid}
end
test "whatever", %{pid: pid} do
assert MyApp.TheWorker.do_stuff(pid)
end
peerreynders
--no-start fixes that
--no-start- does not start applications after compilation
https://hexdocs.pm/mix/Mix.Tasks.Test.html#module-command-line-options
So mix test without --no-start runs integration tests while mix test --no-start only runs the fast isolated tests.
al2o3cr
The usual idiom would be to have another process, external to the worker, that holds onto that state and then supplies it to the worker when requested (maybe in the worker’s handle_continue?)
One possible sequence of events would look like:
-
supervisor boots the “state fetcher/holder” process
-
fetcher/holder boots up
-
(optional) if in production, the fetcher/holder can make the
get_api_responsecall fromhandle_continue. Otherwise, that call is deferred until the worker needs the data. -
supervisor starts the worker process
-
worker process makes a blocking
callfrom itshandle_continueto the holder process to get the state -
in production, that call returns promptly once
get_api_responsehas completed -
in test, that call would block (maybe with
receive?) waiting for the go-ahead from the test setup code -
if the worker process crashes, the fetcher/holder can supply the already-loaded state to it when it restarts







