Disable GenServer in test environment

So I have this common OTP application, starting up several (mostly Phoenix related) children. One of them is a GenServer I created to poll an external TCP socket every second. This is something I don’t want to do in the test environment. Is the following snippet the “common” way to disable this within my application?

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

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Start the Ecto repository
      Example.Repo,
      # Start the TCP poller
      poller_spec(),
      # Start the Telemetry supervisor
      ExampleWeb.Telemetry,
      # Start the PubSub system
      {Phoenix.PubSub, name: Example.PubSub},
      # Start the Endpoint (http/https)
      ExampleWeb.Endpoint
    ]

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

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  @impl true
  def config_change(changed, _new, removed) do
    ExampleWeb.Endpoint.config_change(changed, removed)
    :ok
  end

  defp poller_spec() do
    if Mix.env() == :test
      %{id: Example.Poller, start: {Function, :identity, [:ignore]}}
    else
      {Example.Poller, host: "example.com"}
    end
  end
end

I searched a lot on Github to find such a pattern (found this one in Dashbit’s Bytepack repo), but there aren’t that many open-source Phoenix examples to peek at. Maybe it is super obvious thing to implement if you have more GenServer experience :sweat_smile:

You can have environment-specific children, but you need to make sure your app always knows what environment it’s running in. Since Mix.env() isn’t available at runtime (in a release), you’ll need to make sure the right value gets compiled into your app’s configuration. So first, in config/config.exs, add:

# If you're using Elixir 1.13+
config :example, :environment, config_env()
# For older versions of Elixir
config :example, :environment, Mix.env()

Then, in your application.ex file:

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

  use Application

  @impl true
  def start(_type, _args) do
    children = 
      Application.get_env(:example, :environment)
      |> children()

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

  defp children(:test) do
    [
      Example.Repo,
      poller_spec(),
      ExampleWeb.Telemetry,
      {Phoenix.PubSub, name: Example.PubSub},
      ExampleWeb.Endpoint
    ]
  end

  defp children(_) do
    [
      Example.Repo,
      poller_spec(),
      ExampleWeb.Telemetry,
      {Phoenix.PubSub, name: Example.PubSub},
      ExampleWeb.Endpoint,
      Example.TheGenServerThatShouldNotStartInTest
    ]
  end
end

Of course, you can eliminate some redundancy:

defp children(:common) do
    [
      Example.Repo,
      poller_spec(),
      ExampleWeb.Telemetry,
      {Phoenix.PubSub, name: Example.PubSub},
      ExampleWeb.Endpoint
    ]
end

defp children(:test), do: children(:common)
defp children(_), do: children(:common) ++ [Example.TheGenServerThatShouldNotStartInTest]

You just need to make sure that your children/1 returns the right stuff to start for the given environment.

Thanks, that helped me getting the logic implemented! In the end, I’ve added an additional configuration value that can be set to false in the test environment.

defp poller_spec() do
  if Application.get_env(:example, :poller_enabled, true)
    {Example.Poller, host: "example.com"}
  else
    %{id: Example.Poller, start: {Function, :identity, [:ignore]}}
  end
end
1 Like

Another scenario which is a bit difficult to figure out; what about the seeds file and any GenServer that doesn’t need to be started? How do I know in the Application if it is fully starting or only the seeds are run.

If I understand the question correctly, you can control your app’s startup using any conditional logic you want. For example, you could have the list of children be nothing but [Example.Repo] if an environment variable called REPO_ONLY is set. You’d then set that environment variable when running your seeds (e.g., REPO_ONLY=true mix run priv/repo/seeds.exs).

This is just a broad example. In the end, your list of child processes to start is just that – a list. You can generate it however you see fit.

In the end I found an even clearer way to prevent the poller from running. By checking if the Phoenix server is about to start as well, I conditionally add the poller spec to the list of children.

defp poller_spec() do
  if Application.get_env(:example, :poller_enabled, true) 
      && Phoenix.Endpoint.server?(:example, ExampleWeb.Endpoint)
    {Example.Poller, host: "example.com"}
  else
    %{id: Example.Poller, start: {Function, :identity, [:ignore]}}
  end
end
1 Like