I feel there is some clarification needed here. The Worker process is a GenServer, that does, some work.
As all GenServers, it’s code is divided into 3 parts:
- The public API that clients can call
- GenServer calls to the process itself
handle_xcalls which do the real work
In the tests I am performing right now, I am testing the public API. Say I have the following code:
defmodule Clint.Worker do
use GenServer
defmodule State do
defstruct conn_pid: nil,
active_streams: [],
worker_id: nil,
opts: %{},
deps: %{}
end
###############
# Public API #
###############
@spec start_link(map) :: Supervisor.on_start
def start_link(args) do
deps =
args
|> Map.get(:deps, %{})
|> build_deps()
worker_id = Map.fetch!(args, :worker_id)
opts = Map.fetch!(args, :opts)
Logger.debug("Starting worker #{inspect worker_id }")
init_state = %State{worker_id: worker_id, opts: opts, deps: deps}
pname = :bananas
GenServer.start_link(__MODULE__, init_state, name: pname)
end
@spec request(atom, integer, charlist, map) :: {:ok, :received}
def request(group_name, worker_id, url, injected_deps \\ %{}) do
deps = build_deps(injected_deps)
GenServer.cast(:bananas, {:fire, url})
{:ok, :received}
end
@spec build_deps(map) :: map
defp build_deps(injected_deps), do:
Map.merge(@default_deps, injected_deps, fn _, a, b -> Map.merge(a, b) end)
#############
# Callbacks #
#############
@impl GenServer
def init(state) do
Process.flag(:trap_exit, true)
{:ok, state, {:continue, :establish_conn}}
end
@impl GenServer
def handle_continue(:establish_conn, state) do
with {:ok, conn_pid} <- Logic.establish_connection do
new_state = %{state | conn_pid: conn_pid}
{:noreply, new_state}
else
{:error, reason} -> {:stop, reason, state}
end
end
@impl GenServer
def handle_cast({:fire, url}, state) do
stream_ref = state.deps.http.get.(state.conn_pid, url)
new_state = %{state | active_streams: [stream_ref | state.active_streams]}
{:noreply, new_state}
end
end
Here the public API (what clients will call) are the start_link and request functions. I am testing the Public API of the worker, the face it shows to the world.
This fits nicely into the “Test the interface, not the implementation” rule of testing, but it is insufficient. For example, using this methodology I can’t test any handle_info calls, unless I create a worker and then send him the exact messages that trigger handle_info (which according to some community members, I should do. I am now trying out this strategy).
Is this Unit testing? Is this integration?
I argue that the Worker is my unit, so I can argue this is unit testing. Some of you will say “You are mixing your state tests with GenServer OTP and therefore this is integration testing”. I wouldn’t say you are wrong, but I will definitely say this is a very thin line, at least for me.
@chrismcg I have never used trace for testing before, and I fail to see how I could apply it here. I need to invest more time into this idea, as it looks really cool!
I am however not sure why I need to wait for the
:erlang.trace(:new, true, [:call, :return_to])
call.
I usually (try to) make my questions and topics as small and isolated as possible, to make the load on the people reading smaller. Sometimes, this comes at the cost of clarity, which I believe is the case here.
My worker does not depend on any HTTP client. It depends on an HTTP contract, which can be implemented by any client I wish. The boundary is well defined, the way I see it ![]()
Perhaps, the trouble here is in making it clear it is a boundary. Perhaps you believe I am testing too much the details of my worker which leads you to think I have no boundaries defined. You are one of the most well educated people in testing I have seen and I find it curious how your opinions differ from mine in so many areas. All I can say to defend myself is that I am following a traditional London style TDD, while injecting the dependencies directly without creating Mock modules, because I believe functional injection is the best way of passing dependencies.
Test first is not always the solution, true. This is actually the refactor of a project that I made and that I consider is a complete disaster. No better time to fix it than now ![]()
In fact, one of the many problem is that this project has not tests at all ![]()
Oh my God. Thank you so much. I don;t want to sound … ungrateful, but I am not well versed in erlang yet, so I have trouble understanding what this actually accomplishes. I can only hope this comes with great documentation for dummies like me ![]()
@sorentwo So you let the tests fail repeatedly until you hit a timeout that is slow enough? I understand you provide a maximum number of tries for the test to run, correct (50 times) ?






















