Testing functions that spawn other processes

I have some code like this:

defmodule MyApp.Message.Dispatcher do
  alias MyApp.TaskSupervisor
  alias MyApp.Message.Foo
  alias MyApp.FooTask

  @spec dispatch(term()) :: {:ok, term()} | {:error, term()}
  def dispatch(%Foo{} = message) do
    Task.Supervisor.start_child(TaskSupervisor, FooTask, :run, [[message]])
  end

  def dispatch(message), do: {:error, {:unknown_message, message}}
end

It receives a struct and depending on the struct it spawns a Task. That Task is calling an external API.

My question now is, how would you test this? Currently I have just added the basics:

defmodule MyApp.Message.DispatcherTest do
  use ExUnit.Case, async: true

  alias MyApp.Message.Dispatcher
  alias MyApp.Message.Foo
  alias MyApp.FooTask

  describe "dispatch/1" do
    test "dispatches a Foo message" do
      foo = %Foo{foo: "bar"}

      assert {:ok, pid} = Dispatcher.dispatch(foo)
      assert is_pid(pid)
    end

    test "returns an error for an unknown message" do
      assert {:error, {:unknown_message, :unknown}} = Dispatcher.dispatch(:unknown)
    end
  end
end

This doesn’t really test much… What else would you test here and how? I could use Mox or something to mock the Task.Supervisor and check that it is called with the correct arguments, but mocking like that feels really annoying to me. You end up injecting mocks all over the place… I’m trying to keep Mox for things like external API’s that I really don’t want to call.

Another option would be to try and add some expectations on the API mock (3rd party API call) that the FooTask is calling and assert on that… but then this test is really coupled to the implementation of the FooTask.

The third option I can think of is to just leave it like this and have some integration/end-to-end tests that exercise this code path and in that way verify that it’s working.

Any suggestions?

I think your DispatcherTest is fine. I think it’s enough to cover FooTask.run() with tests. Since Task.Supervisor is standard lib, I kind of just assume it’s tested within Elixir.

1 Like

So one thing that your test does not do is checking if a correct task was spawned.

There are several ways to do that but they all would require changing your code.

What I would probably do is separate the “router” from “dispatcher”. I would just create a MyApp.Message.Dispatcher.Router that has one function that you give it a struct, and it gives you back the atom of task module to spawn. You can test the mapping of messages to tasks this way.

Other than that, I think it’s fine if this module does’t have much tests as it doesn’t really do much.

2 Likes