Make sure your views are bug-free by testing them with elm-test
2018-11-21

You have a bug in your view that only shows up sometimes, under as-yet-undetermined conditions, but the problem is that this view is part of a specific workflow in the application, so you have to get to the right page, fill out a form and click through a couple of dialogs before you can even see if the view is displaying correctly. If not, it’s back to tweaking the code to introduce a potential fix, then reload the application, and start over to get to the view again. Does this sound familiar?

Like any other code, views can have bugs. However, they’re often tested manually, which means that you have to keep going through the same steps over and over to test changes. Tedious.

Fortunately, as of version 1.2.0, elm-explorations/test includes a few new modules which allow us to test view code and hopefully avoid the need for some of the laborious manual testing. (These modules have been migrated from a separate package, eeue56/elm-html-test, by Aaron VonderHaar - thanks Aaron!)

Let’s look at a couple of simple examples. Suppose that we have a login form view written like this:

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)

type Msg
    = Login


viewLogin : Html Msg
viewLogin =
    div [ class "login" ]
        [ input [ placeholder "Organisation", value "" ] []
        , input [ placeholder "User name", value "" ] []
        , input
            [ placeholder "Password"
            , type_ "password"
            , value ""
            ]
            []
        , button [ onClick Login ] [ Html.text "Login" ]
        ]

We can test that this function correctly generates the login button in this manner:

import Test.Html.Query as Query
import Test.Html.Selector as Selector

-- ...

test "Login form has a Login button" <|
    \() ->
        viewLogin
            |> Query.fromHtml
            |> Query.find [ Selector.tag "button" ]
            |> Query.has [ Selector.text "Login" ]

This is just a regular test that you would include in your test suite that’s exercised by elm-test.

First, we have to transform the Html value using Query.fromHtml. Then, we find the relevant element using Query.find, and finally check that its text is “Login”. The Query module provides a number of other functions.

Another test could ensure that the password field has the type attribute set to “password” (so that it doesn’t accidentally get replaced with a text field):

test "Password field is of the right type" <|
    \() ->
        viewLogin
            |> Query.fromHtml
            |> Query.find [ Selector.attribute <| type_ "password" ]
            |> Query.has [ Selector.tag "input" ]

We can also test that clicking the login button produces the Login message (after importing Test.Html.Event):

import Test.Html.Event as Event
import Test.Html.Query as Query
import Test.Html.Selector as Selector

test "Login form generates the message" <|
    \() ->
        viewLogin
            |> Query.fromHtml
            |> Query.find [ tag "button" ]
            |> Event.simulate Event.click
            |> Event.expect Login

In this case, we have to find the button first (with Query.find), then simulate a click event on it with Event.simulate Event.click, and then verify that it results in the Login message. The Event module provides functions for simulating a number of other interactions.

Interested in learning more about testing Elm code?

In my book, Practical Elm, I talk a lot more about testing. You can learn how to write unit tests and property/fuzz tests with elm-test, and how to test your update function with random lists of messages. Actually, there’s a whole chapter on preventing and finding bugs, so for example you can learn how to leverage the type system to that end and how to use the time-travelling Elm debugger. Check it out!

Would you like to dive further into Elm?
📢 My book
Practical Elm
skips the basics and gets straight into the nuts-and-bolts of building non-trivial apps.
🛠 Things like building out the UI, communicating with servers, parsing JSON, structuring the application as it grows, testing, and so on.
Practical Elm