Paul Fioravanti
Paul Fioravanti

Reputation: 16793

Testing captured IO from a spawned process

I want to test the return value and the IO output on the following method:

defmodule Speaker do
  def speak do
    receive do
      { :say, msg } ->
        IO.puts(msg)
        speak
      _other ->
        speak # throw away the message
    end
  end
end

In the ExUnit.CaptureIO docs, there is an example test that does this which looks like the following:

test "checking the return value and the IO output" do
  fun = fn ->
    assert Enum.each(["some", "example"], &(IO.puts &1)) == :ok
  end
  assert capture_io(fun) == "some\nexample\n"
end

Given that, I thought I could write the following test that performs a similar action but with a spawned process:

test ".speak with capture io" do
  pid = Kernel.spawn(Speaker, :speak, [])
  fun = fn ->
    assert send(pid, { :say, "Hello" }) == { :say, "Hello" }
  end
  assert capture_io(fun) == "Hello\n"
end

However, I get the following error message telling me there was no output, even though I can see output on the terminal:

1) test .speak with capture io (SpeakerTest)
   test/speaker_test.exs:25
   Assertion with == failed
   code: capture_io(fun) == "Hello\n"
   lhs:  ""
   rhs:  "Hello\n"
   stacktrace:
     test/speaker_test.exs:30: (test)

So, am I missing something perhaps with regards to testing spawned processes or methods that use the receive macro? How can I change my test to make it pass?

Upvotes: 3

Views: 1174

Answers (2)

alejandrodnm
alejandrodnm

Reputation: 5630

I had a similar problem, I had a registered process on my Application that would timeout every 10 seconds and write to stdio with IO.binwrite, to simulate multiple timeouts I took upon @Pawel-Obrok answer, but change it as to reply the :io_request with an :io_reply, that way the process would not hang allowing me to send multiple messages.

defp assert_io() do
  send(MyProcess, :timeout)
  receive do
    {:io_request, _, reply_as, {:put_chars, _, msg}} ->
      assert msg == "Some IO message"
      send(Stats, {:io_reply, reply_as, :ok})

    _ ->
      flunk
  end
end

test "get multiple messages" do
  Process.group_leader(Process.whereis(MyProcess), self())
  assert_io()
  assert_io()
end

If you want to know more about the IO protocol take a look at the erlang docs about it.

Upvotes: 0

Paweł Obrok
Paweł Obrok

Reputation: 23164

CaptureIO might not be suited for what you're trying to do here. It runs a function and returns the captured output when that function returns. But your function never returns, so seems like this won't work. I came up with the following workaround:

test ".speak with capture io" do
  test_process = self()
  pid = spawn(fn ->
    Process.group_leader(self(), test_process)
    Speaker.speak
  end)

  send(pid, {:say, "Hello"})

  assert_receive {:io_request, _, _, {:put_chars, :unicode, "Hello\n"}}

  # Just to cleanup pid which dies upon not receiving a correct response
  # to the :io_request after a timeout
  Process.exit(pid, :kill)
end

It uses Process.group_leader to set the current process as the receiver of IO messages for the tested process and then asserts that these messages arrive.

Upvotes: 5

Related Questions