GenServers - how to test with `capture_io`?

I’ve got a Genserver that writes messages to stdout. I can’t figure out how to test using capture_io. Documentation says By default, capture_io replaces the group_leader (:stdio) for the current process. Perhaps there is a Process.group_leader call that will make this work, but I haven’t been able to find it. Can anyone point me to a solution?

Here’s a failing test script…

#!/usr/bin/env elixir 

ExUnit.start()

defmodule MyGenServer do
  use GenServer

  def start_link(default \\ []) do
    GenServer.start_link(__MODULE__, default, name: __MODULE__)
  end

  def init(initial_state) do
    {:ok, initial_state}
  end

  def handle_call(:sayhi, _caller, state) do
    IO.puts("HI")
    {:reply, :ok, state}
  end
end

defmodule MyGenServerTest do
  use ExUnit.Case

  import ExUnit.CaptureIO

  test "GenServer responds to :sayhi call by writing 'HI' to stdout" do
    {:ok, pid} = MyGenServer.start_link()
    assert capture_io(fn -> GenServer.call(pid, :sayhi) end) == "HI\n"
  end
end
1 Like

I could make it work with capture_log + import ExUnit.CaptureLog + require Logger inside both modules + actually using Logger.debug (or any other level) and not IO.puts but curiously enough I wasn’t able to make it work with capture_io like in your original script. Also tried with with_io but no dice again. Interesting.

2 Likes

Ya, this was the closest I could find to an answer which is probably something you’ve already come across at this point. I may be brain-farting due to fatigue but I couldn’t quite get it to work with GenServer. I did run into this a while back so I’m pretty curious. It’s not in code I have access to anymore but I think I ended up just going with the repeated advice I keep coming across which is to test behaviour by asserting on the callbacks directly then I guess you can just assert_receive to test the API layer? I honestly don’t quite remember what I did. Sorry if this isn’t very helpful.

2 Likes

The problem is that you have to set the group leader first, which doesn’t seem possible with the way that capture_io/1 currently works:

# group leader is set before starting the GenServer, so this works
assert ExUnit.CaptureIO.capture_io(fn ->
  {:ok, pid} = MyGenServer.start_link()
  GenServer.call(pid, :sayhi)
  GenServer.stop(pid)
end) == "HI\n"

I wonder what the ideal way to do this would be. If we had some way of telling capture_io to use the pid of the GenServer, then it could properly set the group leader.

Maybe something like this:

{:ok, pid} = MyGenServer.start_link()
assert ExUnit.CaptureIO.capture_io([pid: pid], fn -> GenServer.call(pid, :sayhi) end)
4 Likes

@voughtdq grateful for your solution - thank you! I thought I had tried all permutations, but not that one! :laughing:

1 Like

Just a heads up that this will now be possible in v1.17.0:

assert ExUnit.CaptureIO.capture_io(pid, fn -> 
  GenServer.call(pid, :sayhi) 
end)
6 Likes

Can you send us a link to where is this stated?

3 Likes

Ah, so @voughtdq was just being humble :slight_smile:

Thanks, @voughtdq!!

1 Like

if you can’t wait for elixir 1.17, you can manually set the group_leader of the pid you want using :erlang.group_leader/2 to your test process and assert_receive on the messages that flow in.

Be careful! The order of the parameters on :erlang.group_leader/2 might not be the order you expect.

3 Likes