"(EXIT) already started:" when testing parts of application

I am trying to write a test that covers the behavior more or less of the entire application - technically these are more integrative tests than strictly unit tests - and I feel as though there is something missing in my understanding of how processes are started and named that is stopping me from being able to test the application the way I want.

How do I instruct the application code of the names assigned to the associated processes it calls on during the run of a test? I have seen similar questions relating to this issue/error but I am too inexperienced to see the answer for my particular context.

I have the following Supervisor, which starts 3 custom written workers and another from this library

defmodule Krown.Supervisor do          
  use Supervisor                       
  # code omitted for brevity  
                                                                                                                                                                                                 
  def start_link(init_arg \\ []) do                                   
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)     
  end                                                                 

  def init(args \\ []) do                                                                            
                                                                                                   
    children = [                                                                                     
      {FileSystem.Worker,                                                                            
       [                                                                                             
         name: Keyword.get(args, :file_event_name, FileEventNotify),                                 
         recursive: true,                                                                            
         dirs: [Application.fetch_env!(:my_app, :event_dir)]                                          
       ]},                                                                                           
      {Dispatcher, [name: Keyword.get(args, :dispatcher_name, Dispatcher)]},  
      {Converter, [name: Keyword.get(args, :converter_name, Converter)]},
      {Uploader, [name: Keyword.get(args, :uploader_name, Uploader)]},           
    ]                                                                                                
                                                                                                   
    Supervisor.init(children, strategy: :one_for_one)                                                
  end                                                                                                
end

The Dispatcher, which monitors messages in FileSystem.Worker, looks like this:

defmodule Krown.Dispatcher do
  use GenServer                                                                                                                     
  # code omitted for brevity                                                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                             
  def init(_) do                                                                                                                    
    FileEventNotify                                                                                                                 
    |> Process.whereis()                                                                                                            
    |> FileSystem.subscribe()                                                                                                       
                                                                                                                                    
    {:ok, nil}                                                                                                                      
  end                                                                                                                               
                                                                                                                                    
  def handle_info({:file_event, _watcher_pid, {fpath, events}}, _state) do                                                                                                                                                                                                                                                                                                                                                                                                                                                          
    Path.dirname(fpath)                                                                                                             
    |> Path.split()                                                                                                                 
    |> List.last()                                                                                                                  
    |> case do                                                                                                                      
      "raw" ->                                                                                                           
        Converter                                                                                                                     
      "wav" ->                                                                                                         
        Uploader                                                                                                      
     end                                                                                                                             
     |> case Process.whereis() do                                                                                             
        nil ->                                                                                                                      
          Logger.warn("Unable to locate pid with name: #{pid_name}")                                                                
          raise "#{pid_name} PID NOT FOUND"                                                                                                                                                                                                                
        pid ->                                                                                                                      
          GenServer.cast(pid, {:perform, fpath})                                                                                    
      end                                                                                                                           
                                                                                                                                    
    {:noreply, nil}                                                                                                                 
  end                                                                                                                               

And a basic test for illustrative purposes:


  test "something contrived "                           
    spec = %{                                                                         
      id: __MODULE__,                                                                    
      start: {                                                                           
        DescribedModule,                                                                 
        :start_link,                                                                     
        [[uploader_name: Test.Uploader,                                                  
          converter_name: Test.Converter,                                                
          file_event_name: Test.FileEventNotify,                                         
          event_dispatcher_name: Test.Dispatcher]]                                       
      }                                                                                  
    }                                                                                    
    pid = start_supervised!(spec, restart: :temporary)                                
             
    # create a file that will trigger FileEventNotify and Dispatcher 
    File.touch("some_fpath")                                                                            
    assert Process.alive?(pid)     

   # Uploader will remove the file once it has completed #perform
    refute File.exists?("some_fpath")
                                                                                                                      
  end                                                                                    

Declaring names for each GenServer in the test avoids the ** (EXIT) already started: #PID<0.284.0> type error, but it effectively breaks my application in the Dispatcher, since the dispatcher relies on there being static names for the two processes it sends messages to in #perform/2.

So I think to myself, “maybe I am missing something, possibly the use of a Registry?”. But I played with that and it took me back to my first problem whereby the application code has no way of knowing what the dynamically named Registry is.

I noticed that if I comment out mod: {Krown, []} from mix.exs, then I could run the tests successfully - but obviously this is not something I can use successfully in the real world, so what’s the point?

Maybe I am thinking about this all wrong. I feel I am stuck in my understanding. I guess I am writing more integration tests vs unit tests? If so, should I structure them differently?
I also have doubts the vocabulary I am using to express the issue I am having, so any feedback on that is also welcome.

1 Like

I’m not 100% this is what you may need, but you can pattern match on the context of the test.

test "something contrived ", %{test: test_name} = _context do
  # test_name is a unique string that you can use to identify your GenServers.
end

you can also use setup/2 to create a genserver automatically, and pass it through the context, but in this case you will have to randomly generate the name as the test name won’t be available.

Also creating a macro that will return the file path and line number works.

defmacrop id() do
  quote do
    {__ENV__.file, __ENV__.line}
   end
end

#and then in your spec
id: id()

or you can call {__ENV__.file, __ENV__.line} directly.