Testing a Phoenix LiveView that does an async operation after mount

Joel Kemp
Joel Kemp
Sep 16, 2020 · 2 min read

I had a LiveView that was avoiding blocking mount by doing its side-effect (fetching data from a backend service) asynchronously. I started thinking “how will I test this if the data fetching is async? How can I make sure that the call is completed by the time I call LiveViewTest.render?”

It all actually works right out of the box due to FIFO execution of messages in a process’ mailbox (thanks Erlang/OTP).

Here’s an example of a simple LiveView that wants to fetch some data from an external source and render the response. It doesn’t want to block the initial render of the LiveView while we wait for the async response, so it sends itself a message.

To be clear, the line in mount send(self(), {:load_data}) queues up a message in this same LiveView process’ mailbox to be processed when the LiveView is done with the mount.

A sanity test for this LiveView will end up looking like the following:

Breaking the test down a bit, it checks that the view renders the response from MyDataFetcher.fetch. We use the Mock library to mock a response of “Hello There” from the LiveView’s call to MyDataFetcher.fetch.

Then we mount MyView once the mock is set up with:

{:ok, view, _html} = live_isolated(conn, MyView)

Then we render MyView so that we can assert that the rendered view has the response “Hello There” somewhere in its html:

assert render(view) =~ response

So what happened to the async fetching of the data and why didn’t we have to do any waiting/sleeping or faking messages to the liveview?

  • As we said, send(self(), {:load_data}) in MyView’s mount queues up a message in MyView’s process mailbox.
  • When we called Phoenix.LiveViewTest.render in the test:render(view), it actually queues up another message in MyView’s process mailbox!
  • A LiveView is a GenServer (intuitively, think of it as an Erlang Process).
  • An Erlang process processes a message from its mailbox in a first-in-first-out (FIFO) fashion; this means it processes the messages in the order they were received. See here and here for details on that.
  • So, the render call’s message is queued up behind/after our {:load_data} message, which means our test assertion is guaranteed to happen after the (mocked) async operation completes. This is why we’ll have the mocked response in the rendered output!

Elixir Learnings