I am testing a Bakeware CLI tool that takes list of servers, opens an SSH connection for each server, and runs the appropriate ssh command on each. The app is very similar to the KV.Registry example in the official Elixir guide Mix and OTP section.
The supervision tree is:
CoordinatorSupervisor (Supervisor, named, strategy: :one_for_all)
- ConnectionSupervisor (DynamicSupervisor, named, strategy: :one_for_one)
- Connection (GenServer, restart: :temporary)
- Connection (GenServer, restart: :temporary)
- …
- Coordinator (GenServer, named)
Connection
deals with creating the SSH connection and passing commands.
Coordinator
deals with starting and managing the different connections.
The main
function takes some command line arguments with a default list of servers, starts the CoordinatorSupervisor
, adds the servers, passes the commands, and waits for results (using a receive
loop).
The problem is the state of the Supervisor’s and GenServer’s are difficult to manage before each test. When the tests start, the supervision tree is alive with the default arguments given to the app at startup.
defmodule CoordinatorTest do
use ExUnit.Case, async: falsetest “adds server” do
my_server = “localhost”
assert Coordinator.list_servers(Coordinator) == {:ok, %{}}Coordinator.add_server(Coordinator, my_server) {:ok, server_list} = Coordinator.list_servers(Coordinator) servers = Map.keys(server_list) assert Enum.member?(servers, my_server)
end
test “removes connection on crash” do
my_server = “localhost”
assert Coordinator.list_servers(Coordinator) == {:ok, %{}}Coordinator.add_server(Coordinator, my_server) {:ok, server_list} = Coordinator.list_servers(Coordinator) [server] = Map.keys(server_list) assert my_server == server conn = Map.get(server_list, server) GenServer.stop(conn, :shutdown) assert Coordinator.list_servers(Coordinator) == {:ok, %{}}
end
end
The assertion that the list of servers is empty (second line of each test) fails because they contain the default servers. As I understand it, when running mix test
, it starts the app and, at some arbitrary point, the tests start. The receive
loop in main
stops the app from shutting down, so the supervision tree is still running with the default servers. I thought that perhaps if I stop the supervisor and restart it:
test “adds server” do
Supervisor.stop(CoordinatorSupervisor)
start_supervised!(CoordinatorSupervisor)
my_server = “localhost”
assert Coordinator.list_servers(Coordinator) == {:ok, %{}}
…
I receive the error (for both tests):
** (exit) exited in: GenServer.stop(CoordinatorSupervisor, :normal, :infinity)
** (EXIT) no process: the process is not alive or there’s no process currently associated with the given name, possibly because its application isn’t started
So when I run the test, CoordinatorSupervisor
is definitely running showing the default server list (making my assertion fail). However, if I run the test and try to stop the supervisor, the supervisor is definitely not running, giving me the error!
If I could stop CoordinatorSupervisor
without an error, I could test Coordinator
(a GenServer
) on its own. Which seems like a good thing, except it uses the ConnectionSupervisor
(a DynamicSupervisor
) to start the connections (and the dynamic supervisor is a child of the supervisor).
Any suggestions to restart the supervision tree before each test, so the server list is blank?