Improving LiveView test reading & writing with a set of helper functions

Hello, all! I want to share a pattern that I have found useful and get your feedback. I am relatively new to LiveView, so I am doing so humbly at risk of beginners naivety.

In short, as I began writing tests of LiveViews, I found it difficult to keep the different parts of the test state organized in my mind. Those being: 1) the conn, 2) the “live”, and 3) the “html” doc that is often asserted against. This would distract me from the actual interactions that the test is meant to describe.

Here is a brief example (contrived, but hopefully realistic):

import Phoenix.LiveViewTest

test "live view", %{conn: conn} do
  {:ok, live, html} = live(conn, "/")
  assert html =~ "New"
  assert live |> element("a.new") |> render_click() =~ "Enter new"
  assert_patch(live, "/new")
  {:ok, _, html} = live |> form(".form", @attrs) |> render_submit() |> follow_redirect(conn)
  assert html =~ "success"
end

By my judgement, this is pretty typical of LiveView tests. In the test, the value that is being handled switches between the conn (starting the view and following redirect), the live view (clicking the link and submitting the form) and the HTML docs (asserting initial page content and changes as links and forms are interacted with). The clicks and forms I found particularly confusing, because the data of focus changes from live view to HTML through the pipeline. Ultimately I just want to focus on the user interactions.

That’s where some helper functions come in. Essentially their purpose is to bundle this context together and make everything a pipeline so that the reader is relieved of mentally juggling the state.

Here’s the same example refactoring with these helpers:

use Iamvery.Phoenix.LiveView.TestHelpers

test "live view", %{conn: conn} do
  start(conn, "/")
  |> assert_html("New")
  |> click("a.new")
  |> assert_html("Enter new")
  |> assert_path("/new")
  |> submit_form(".form", @attrs)
  |> assert_html("success")
end

As you can see, I’ve bundled these up (for now) as a part of a utility package that I maintain, but if they’re well-received, I could see them becoming a part of the LiveView package itself. With some more work, of course, they’re not meant to be a complete set at this time.

Here is a link to the package source and a blog post I put together as well:

Let me know what you think!

4 Likes

Also, fwiw, the only thing I’m really concerned about here is developer experience. I’m not deeply attached to the naming or API of any of this. In fact, I all but assume there’s a better way to express and implement the purpose given more eyes and experience on the problem itself.

1 Like

I love it, really cool idea! Nice work :clap:

  1. When a test fails, do I see which step in pipeline caused it to fail?

  2. I would personally love to have an assert_flash :slight_smile:

3 Likes

Great question! And the answer is: There is room for improvement. Honestly, I’m not in love with the way the macro generations functions. I didn’t put a ton of thought into it, so there’s probably a better API for the helpers.

Here’s an example failure:

  1) test copying a stream from source (MyApp.SomeTest)
     test/features/some_test.exs:13
     Assertion with =~ failed
     code:  assert html =~ expected_html
     left:  "<html>..."
     right: "lolwat"
     stacktrace:
       test/features/some_test.exs:4: MyApp.SomeTest.assert_html/2
       test/features/some_test.exs:28: (test)

The first line in the trace is the use call, but the second line does point to the correct place in the test file, i.e. the call to assert_html.

Great idea! I’m sure there are lot’s of other things that could be helpful :slight_smile:

1 Like

Some quick reactions:

  • asserting on the HTML with =~ is brittle if your data sometimes contains characters that are escaped in HTML; a favorite offender is Faker which will occasionally generate last names like O'Henry which become O&apos;Henry. I’ve personally been using has_element? on the view which deals with details like that automatically.

  • having the :ok part in the function heads feels strange; if the call to live/2 in start doesn’t return the expected shape, it will result in a MatchError that points at the next function in the chain.

  • I have a vague recollection that stacktraces didn’t always used to point to the correct line in older Elixir versions, but I don’t have any evidence to support that (and might be misremembering a factoid about Ruby method chains!). That may be part of why this style isn’t widely used.

1 Like

I ran into that too, which is why I included an escape/1 function that’s just a delegate to Plug.HTML.html_escape/1. It can be used like:

|> assert_html(escape("can't be saved"))

More generally though, I also suspected that =~ was a little too prescriptive for a function with such a general name as “assert”. Perhaps match_html is a better name? Or coming up with a way of providing the full strategy. Here’s a (probably bad) example:

|> assert_html(& &1 =~ escape("can't be saved"))

Update: Here’s an issue for this idea Is assert_html too prescriptive? · Issue #4 · iamvery/iamvery-elixir · GitHub

Great feedback! That makes a lot of sense to me. For whatever reason this hasn’t come up yet in my application of the pattern, but my guess would be to provide more function heads to handle not-OK scenarios in the pipeline. I’ll try to find some practical examples and consider solutions.

Update: I put this in an issue What if things are not :ok? · Issue #3 · iamvery/iamvery-elixir · GitHub.

I would love to get feedback on other ways to include the helpers. Something still doesn’t sit right with me on generating the functions with the “using” macro, but other efforts fell flat at the time. Still very open to improving that.

Thanks for the thoughtful reply!

I started an issue about the assumed :ok in the function heads. What if things are not :ok? · Issue #3 · iamvery/iamvery-elixir · GitHub