ExUnit: start_supervisor/2 is persisting process state between tests

supervisor
exunit

#1

Hello all. I am having an issue where start_supervisor/2 and stop_supervisor/1 aren’t working as expected in ExUnit tests. In each test case I call start_supervisor/2, and after the test I call stop_supervisor/1 so that the process under test begins anew. However, the process state somehow persists between tests! Here is the source code

defmodule NectarDb.OtherNodes do
  use Agent

  @me __MODULE__

  @spec start_link(any) :: {:ok, pid}  
  def start_link(_args) do
    Agent.start_link(fn -> [] end, name: @me)
  end

  @spec add_node(String.t) :: :ok
  def add_node(node) do
    Agent.update(@me, fn nodes -> [node | nodes] end)
  end

  @spec get_nodes() :: [String.t]
  def get_nodes() do
    Agent.get(@me, fn nodes -> nodes end)
  end
end

and here is the test code:

defmodule NectarDb.OtherNodesTest do
  use ExUnit.Case, async: false

  alias NectarDb.OtherNodes

  describe "adding and retrieving nodes from OtherNodes" do
    test "succeeds for addition" do
      start_supervised({OtherNodes,[]})
      
      OtherNodes.add_node("a@a")
      assert ["a@a"] == OtherNodes.get_nodes()

      :ok = stop_supervised(OtherNodes)
    end

    test "succeeds for multiple additions" do
      start_supervised({OtherNodes,[]})

      OtherNodes.add_node("a@a")
      OtherNodes.add_node("a@b")
      OtherNodes.add_node("a@c")
      assert ["a@c","a@b","a@a"] == OtherNodes.get_nodes()
      stop_supervised(OtherNodes)
      
    end
  end
end

#2

I would also like to add that my application starts these Agents as workers, which may be causing the issues

defmodule NectarDb.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  import Supervisor.Spec

  alias NectarDb.Oplog
  alias NectarDb.OtherNodes
  alias NectarDb.Store
  
  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      # Starts a worker by calling: NectarDb.Worker.start_link(arg)
      # {NectarDb.Worker, arg},
      worker(Oplog,[nil]),
      worker(OtherNodes,[nil]),    
      worker(Store,[nil]),
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: NectarDb.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

#3

Yes, the workers we are trying to start on test is going to conflict with the ones in your application. You need to make the name a parameter and pass different ones.


#4

Could someone (@josevalim, if you have time) expand on this?

something like this?:

  def start_link(test_name) do
    Supervisor.start_link(__MODULE__, :ok, name: test_name)
  end

  def start_link() do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

ATM I’m getting

 Application heroes_engine exited: HeroesEngine.Application.start(:normal, []) returned an error: shutdown: failed to start child: HeroesEngine.HeroesSupervisor
    ** (EXIT) an exception was raised:
        ** (ArgumentError) expected :name option to be one of the following:

  * nil
  * atom
  * {:global, term}
  * {:via, module, term}

Got: []

code at https://gitlab.com/smedegaard/heroes_engine


#5

When you don’t pass a tuple to start_supervised, we call start_link([]) and not start_link(). :slight_smile:


#6

Thanks a bunch for the reply Jose!

But I’m afraid I just can’t wrap my head around how start_supervised works. It might just be a lack of my knowledge of OTP.

I can’t seem to get the function to pass any arguments. In desperation I tried

start_supervised!(HeroesSupervisor)
start_supervised!(HeroesSupervisor,{:test})
start_supervised!(HeroesSupervisor, {name: :test})
start_supervised!({HeroesSupervisor, {:test}})

and the same for non-bang version start_supervised
But it seems to pass [] no mater what I do.

I ended up matching on [] just to see if it got me somewhere.

It sort of did

defmodule HeroesEngine.HeroesSupervisor do
  use Supervisor

  def start_link([]) do
    Supervisor.start_link(__MODULE__, :ok, name: :test)
  end

  def start_link() do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

...

Now it calls start_link([]) but that leads me back to my original problem of the process already being started.

  ** (RuntimeError) failed to start child with the spec HeroesEngine.HeroesSupervisor.
     Reason: already started: #PID<0.158.0>

If anyone has some example code showing how to pass an argument to a Supervisor from start_supervised or start_supervised! I’d love to take a look. Any advice is appreciated :bowing_man:


#7

The first argument to start_supervised is the child_spec, so in your case I believe you want:

start_supervised!({HeroesSupervisor, :test})

#8

Thanks again!

I got past the errors and I’ve learned something new :raised_hands: :tada:

For future reference:

My GenServer and it’s child_spec

defmodule HeroesEngine do
  use GenServer, start: {__MODULE__, :start_link, []}, restart: :transient, type: :worker
  ...

My supervisor:

defmodule HeroesEngine.HeroesSupervisor do
  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, :ok, name: :test)
  end

  def start_link() do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def init(:ok) do
    Supervisor.init([HeroesEngine], strategy: :simple_one_for_one)
  end

...

My test setup

defmodule HeroesEngineTest do

...

setup do
    # Random list_name so I avoid testing on an old state
    # Might be a more bullet proof way to randomize?
    list_name =
      Enum.random(1..1_000_000)
      |> Integer.to_string

    pid = start_supervised!(HeroesSupervisor,
      [start: {HeroesEngine, :start_link, [list_name]}, restart: :transient, type: :worker])

    hero = Hero.new(@hero_name, @hero_power)

    [hero: hero, pid: pid]
  end

...

Example test

  test "add_hero() adds a hero to list", context do
    {:ok, {:hero_added, new_state}} = HeroesEngine.add_hero(context[:pid], context[:hero])
    assert Map.has_key?(new_state[:heroes], String.to_atom(@hero_name))
  end