Sunday, July 14, 2024
HomeTechnologySoftwareTest Drive HTML Template

Test Drive HTML Template

A decade or more later, single-page applications generated by JavaScript frameworks has become the normwe’re seeing a resurgence in popularity of server-side rendered HTML, also thanks to things like HTMX or turbine. Now, writing rich web UIs in traditional server-side languages ​​like Go or Java is not only possible, but a very attractive proposition.

Then we are faced with the problem of writing automated tests for the HTML portion of the web application.Although the JavaScript world continues to evolve powerful and complicated Methods for testing UI range from unit level to integrated to end-to-end. In other languages ​​we do not have such a rich set of available tools.

When writing web applications in Go or Java, HTML is typically generated through templates, which contain small pieces of logic. Of course it is possible to test them indirectly through end-to-end tests, but these tests are slow and expensive.

We can write unit tests that use CSS selectors to detect the presence and correct content of specific HTML elements in a file. Parameterizing these tests makes it easy to add new tests and clearly indicates what details each test is verifying. This approach works for any language that has access to an HTML parsing library that supports CSS selectors; examples are provided in Go and Java.

motivation

Why test HTML templates? After all, the most reliable way to check if a template is valid is to render it as HTML and open it in a browser, right?

There is some truth to this; unit tests cannot prove that templates work as expected when rendered in a browser, so they need to be checked manually. If we make a mistake in the logic of the template, usually The template is broken in an obvious way, so errors are quickly discovered.

on the other hand:

  • Relying solely on manual testing is risky; what if we make a change that breaks the template, and we don’t test it because we think it won’t affect the template? We’ll get an error at runtime!
  • Templates often contain logic, such as if-then-else or iteration of an array of items, and we often need to display different content when the array is empty.With all these bits of logic, manually checking all cases quickly becomes unsustainable
  • Some errors are not visible in the browser. Browsers are very tolerant of inconsistencies in HTML, relying on heuristics to fix broken HTML, but then we may get different results in different browsers and on different devices. It’s a good idea to check that the HTML structure we established in the template is what we want it to be.

It turns out that test-driven HTML templates are easy; let’s see how to do it in Go and Java.As a starting point I will use TodoMVC templatea sample application that showcases JavaScript frameworks.

As long as we have access to a suitable HTML parser, we’ll see techniques that can be applied to any programming language and template technology.

This article is a bit long; you might want to read
Final solution in Go or
Javaneseor Skip to conclusion.

Level 1: Check that the HTML is correct

The first thing we want to check is that the HTML we generated is basically correct. What I mean is not checking whether the HTML is valid according to the W3C; I mean checking whether the HTML is valid. It would be cool to do this, but it’s better to start with simpler, faster checks.For example, we want our tests to break if the template produces something like

<div>foo</p>

Let’s see how to do it in stages: We start with the following test that tries to compile the template.In Go we use the standard html/template pack.

go

  func Test_wellFormedHtml(t *testing.T) {
    templ := template.Must(template.ParseFiles("index.tmpl"))
    _ = templ
  }

In Java we use beard
Because it is so simple to use; free mark or
speed are other common choices.

Java

  @Test
  void indexIsSoundHtml() {
      var template = Mustache.compiler().compile(
              new InputStreamReader(
                      getClass().getResourceAsStream("/index.tmpl")));
  }

If we run this test, it will fail because index.tmpl file does not exist. So we create it using the broken HTML above. The test should now pass.

Then we create a Model For template use. The application manages a to-do list and we can create a minimal mockup for demonstration purposes.

go

  func Test_wellFormedHtml(t *testing.T) {
    templ := template.Must(template.ParseFiles("index.tmpl"))
    model := todo.NewList()
    _ = templ
    _ = model
  }

Java

  @Test
  void indexIsSoundHtml() {
      var template = Mustache.compiler().compile(
              new InputStreamReader(
                      getClass().getResourceAsStream("/index.tmpl")));
      var model = new TodoList();
  }

Now we render the template, saving the result in a byte buffer (Go) or as String (Java).

go

  func Test_wellFormedHtml(t *testing.T) {
    templ := template.Must(template.ParseFiles("index.tmpl"))
    model := todo.NewList()
    var buf bytes.Buffer
    err := templ.Execute(&buf, model)
    if err != nil {
      panic(err)
    }
  }

Java

  @Test
  void indexIsSoundHtml() {
      var template = Mustache.compiler().compile(
              new InputStreamReader(
                      getClass().getResourceAsStream("/index.tmpl")));
      var model = new TodoList();
  
      var html = template.execute(model);
  }

At this point, we want parse HTML, we expect to see an error because in our corrupted HTML there is a div element enclosed by a p element. There is an HTML parser in the Go standard library, but it is too permissive: if we run it on broken HTML, we don’t get an error.Fortunately, the Go standard library also has an XML parser that can be configured to parse HTML (thanks This stack overflow answer)

go

  func Test_wellFormedHtml(t *testing.T) {
    templ := template.Must(template.ParseFiles("index.tmpl"))
    model := todo.NewList()
    
    // render the template into a buffer
    var buf bytes.Buffer
    err := templ.Execute(&buf, model)
    if err != nil {
      panic(err)
    }
  
    // check that the template can be parsed as (lenient) XML
    decoder := xml.NewDecoder(bytes.NewReader(buf.Bytes()))
    decoder.Strict = false
    decoder.AutoClose = xml.HTMLAutoClose
    decoder.Entity = xml.HTMLEntity
    for {
      _, err := decoder.Token()
      switch err {
      case io.EOF:
        return // We're done, it's valid!
      case nil:
        // do nothing
      default:
        t.Fatalf("Error parsing html: %s", err)
      }
    }
  }

source

This code configures the HTML parser to have the appropriate leniency level for HTML, and then parses the HTML tokens one by one. In fact, we see the error message we wanted:

--- FAIL: Test_wellFormedHtml (0.00s)
    index_template_test.go:61: Error parsing html: XML syntax error on line 4: unexpected end element </p>

In Java, the general purpose libraries available are Soup:

Java

  @Test
  void indexIsSoundHtml() {
      var template = Mustache.compiler().compile(
              new InputStreamReader(
                      getClass().getResourceAsStream("/index.tmpl")));
      var model = new TodoList();
  
      var html = template.execute(model);
  
      var parser = Parser.htmlParser().setTrackErrors(10);
      Jsoup.parse(html, "", parser);
      assertThat(parser.getErrors()).isEmpty();
  }

source

We see it fails:

java.lang.AssertionError: 
Expecting empty but was:<(<1:13>: Unexpected EndTag token (</p>) when in state (InBody),

success!Now if we copy Contents of the TodoMVC template for us index.tmpl File, test passed.

However, the test is too verbose: we extract two helper functions in order to make the intent of the test clearer, and we get

go

  func Test_wellFormedHtml(t *testing.T) {
    model := todo.NewList()
  
    buf := renderTemplate("index.tmpl", model)
  
    assertWellFormedHtml(t, buf)
  }

source

Java

  @Test
  void indexIsSoundHtml() {
      var model = new TodoList();
  
      var html = renderTemplate("/index.tmpl", model);
  
      assertSoundHtml(html);
  }

source

Level 2: Test HTML structure

What else should we test?

We know that the appearance of a page can ultimately only be tested by humans observing how it renders in a browser. However, there is often logic in the template and we want to be able to test that logic.

One might be tempted to test rendered HTML for string equality, but this technique fails in practice because templates contain a lot of detail that makes string equality assertions impractical. The assertion becomes very verbose and when reading the assertion it is difficult to understand what we are trying to prove.

What we need is a technique to assert some parts The rendered HTML matches our expectations, and Ignore all the details that we don’t care about. One way to do this is to execute the query using CSS selector language: It is a powerful language that allows us to select the elements we care about from the entire HTML file. Once we select these elements, we (1) calculate whether the number of elements returned is what we expect, and (2) they contain the text or other content we expect.

The UI we should produce looks like this:

There are several details of dynamic rendering:

  1. Significant changes in the number of items and their text content
  2. The style of a to-do item changes when completed (e.g. the second one)
  3. The “2 items left” text will change depending on the number of unfinished items.
  4. One of the three buttons “All”, “Activities”, and “Completed” will be highlighted, depending on the current URL; for example, if we decide to only display the URL for the “Activities” item, it will be /activethen the current url is /activethe “Activity” button should be surrounded by a thin red rectangle
  5. The “Clear Completed” button should only be visible if any item has been completed

Each problem can be tested with the help of CSS selectors.

This is a snippet of the TodoMVC template (slightly simplified). I haven’t added the dynamic bit yet, so what we see here is static content, provided as an example:

index.tmpl

  <section class="todoapp">
    <ul class="todo-list">
      <!-- These are here just to show the structure of the list items -->
      <!-- List items should get the class `completed` when marked as completed -->
      <li class="completed">  
        <div class="view">
          <input class="toggle" type="checkbox" checked>
          <label>Taste JavaScript</label> 
          <button class="destroy"></button>
        </div>
      </li>
      <li>
        <div class="view">
          <input class="toggle" type="checkbox">
          <label>Buy a unicorn</label> 
          <button class="destroy"></button>
        </div>
      </li>
    </ul>
    <footer class="footer">
      <!-- This should be `0 items left` by default -->
      <span class="todo-count"><strong>0</strong> item left</span> 
      <ul class="filters">
        <li>
          <a class="selected" href="#/">All</a> 
        </li>
        <li>
          <a href="#/active">Active</a>
        </li>
        <li>
          <a href="#/completed">Completed</a>
        </li>
      </ul>
      <!-- Hidden if no completed items are left ↓ -->
      <button class="clear-completed">Clear completed</button> 
    </footer>
  </section>  

source

By looking at the static version of the template, we can infer which CSS selectors can be used to identify relevant elements for the 5 dynamic features listed above:

feature CSS selectors
All items ul.todo-list li
Completed projects ul.todo-list li.completed completed
remaining items span.todo-count
Highlighted navigation links ul. filter a.selected
Clear Done button Button.Clear-Done

We can use these selectors to focus our tests on what we want to test.

Test HTML content

The first test will look for All itemsand prove that the data of the test setting is rendered correctly.

func Test_todoItemsAreShown(t *testing.T) {
  model := todo.NewList()
  model.Add("Foo")
  model.Add("Bar")

  buf := renderTemplate(model)

  // assert there are two <li> elements inside the <ul class="todo-list"> 
  // assert the first <li> text is "Foo"
  // assert the second <li> text is "Bar"
}

We need a way to query HTML files using CSS selectors; a good Go library is Inquire, which implements an API inspired by jQuery.In Java, we continue to use the same library used for testing sane HTML, namely
Soup. Our quiz becomes:

go

  func Test_todoItemsAreShown(t *testing.T) {
    model := todo.NewList()
    model.Add("Foo")
    model.Add("Bar")
  
    buf := renderTemplate("index.tmpl", model)
  
    // parse the HTML with goquery
    document, err := goquery.NewDocumentFromReader(bytes.NewReader(buf.Bytes()))
    if err != nil {
      // if parsing fails, we stop the test here with t.FatalF
      t.Fatalf("Error rendering template %s", err)
    }
  
    // assert there are two <li> elements inside the <ul class="todo-list">
    selection := document.Find("ul.todo-list li")
    assert.Equal(t, 2, selection.Length())
  
    // assert the first <li> text is "Foo"
    assert.Equal(t, "Foo", text(selection.Nodes(0)))
  
    // assert the second <li> text is "Bar"
    assert.Equal(t, "Bar", text(selection.Nodes(1)))
  }
  
  func text(node *html.Node) string {
    // A little mess due to the fact that goquery has
    // a .Text() method on Selection but not on html.Node
    sel := goquery.Selection{Nodes: ()*html.Node{node}}
    return strings.TrimSpace(sel.Text())
  }

source

Java

  @Test
  void todoItemsAreShown() throws IOException {
      var model = new TodoList();
      model.add("Foo");
      model.add("Bar");
  
      var html = renderTemplate("/index.tmpl", model);
  
      // parse the HTML with jsoup
      Document document = Jsoup.parse(html, "");
  
      // assert there are two <li> elements inside the <ul class="todo-list">
      var selection = document.select("ul.todo-list li");
      assertThat(selection).hasSize(2);
  
      // assert the first <li> text is "Foo"
      assertThat(selection.get(0).text()).isEqualTo("Foo");
  
      // assert the second <li> text is "Bar"
      assertThat(selection.get(1).text()).isEqualTo("Bar");
  }

source

If we still haven’t changed the template to populate the list from the model, this test will fail because the static template todo has different text:

go

  --- FAIL: Test_todoItemsAreShown (0.00s)
      index_template_test.go:44: First list item: want Foo, got Taste JavaScript
      index_template_test.go:49: Second list item: want Bar, got Buy a unicorn

Java

  IndexTemplateTest > todoItemsAreShown() FAILED
      org.opentest4j.AssertionFailedError:
      Expecting:
       <"Taste JavaScript">
      to be equal to:
       <"Foo">
      but was not.

We fix it by making the template use model data:

go

  <ul class="todo-list">
    {{ range .Items }}
      <li>
        <div class="view">
          <input class="toggle" type="checkbox">
          <label>{{ .Title }}</label>
          <button class="destroy"></button>
        </div>
      </li>
    {{ end }}
  </ul>

source

Java-jmustache

  <ul class="todo-list">
    {{ #allItems }}
    <li>
      <div class="view">
        <input class="toggle" type="checkbox">
        <label>{{ title }}</label>
        <button class="destroy"></button>
      </div>
    </li>
    {{ /allItems }}
  </ul>

source

Test content and sanity simultaneously

Our tests work, but are a bit lengthy, especially the Go version. If we were to make more tests, they would become repetitive and difficult to read, so we made it more concise by extracting a helper function for parsing the html.We also removed comments because the code should be clear

go

  func Test_todoItemsAreShown(t *testing.T) {
    model := todo.NewList()
    model.Add("Foo")
    model.Add("Bar")
  
    buf := renderTemplate("index.tmpl", model)
  
    document := parseHtml(t, buf)
    selection := document.Find("ul.todo-list li")
    assert.Equal(t, 2, selection.Length())
    assert.Equal(t, "Foo", text(selection.Nodes(0)))
    assert.Equal(t, "Bar", text(selection.Nodes(1)))
  }
  
  func parseHtml(t *testing.T, buf bytes.Buffer) *goquery.Document {
    document, err := goquery.NewDocumentFromReader(bytes.NewReader(buf.Bytes()))
    if err != nil {
      // if parsing fails, we stop the test here with t.FatalF
      t.Fatalf("Error rendering template %s", err)
    }
    return document
  }

Java

  @Test
  void todoItemsAreShown() throws IOException {
      var model = new TodoList();
      model.add("Foo");
      model.add("Bar");
  
      var html = renderTemplate("/index.tmpl", model);
  
      var document = parseHtml(html);
      var selection = document.select("ul.todo-list li");
      assertThat(selection).hasSize(2);
      assertThat(selection.get(0).text()).isEqualTo("Foo");
      assertThat(selection.get(1).text()).isEqualTo("Bar");
  }
  
  private static Document parseHtml(String html) {
      return Jsoup.parse(html, "");
  }

much better! At least, that’s what I thought.Now we extract parseHtml helper, it’s a good idea to check that the HTML in the helper is correct:

go

  func parseHtml(t *testing.T, buf bytes.Buffer) *goquery.Document {
    assertWellFormedHtml(t, buf)
    document, err := goquery.NewDocumentFromReader(bytes.NewReader(buf.Bytes()))
    if err != nil {
      // if parsing fails, we stop the test here with t.FatalF
      t.Fatalf("Error rendering template %s", err)
    }
    return document
  }

source

Java

  private static Document parseHtml(String html) {
      var parser = Parser.htmlParser().setTrackErrors(10);
      var document = Jsoup.parse(html, "", parser);
      assertThat(parser.getErrors()).isEmpty();
      return document;
  }

source

This way, we can get rid of the first test we wrote, since we’re now testing sane HTML all the time.

second test

Now we are in a good position to test more rendering logic.The second dynamic feature in our list is “The list item should get the class
completed When marked as completed”. We can write a test for this:

go

  func Test_completedItemsGetCompletedClass(t *testing.T) {
    model := todo.NewList()
    model.Add("Foo")
    model.AddCompleted("Bar")
  
    buf := renderTemplate("index.tmpl", model)
  
    document := parseHtml(t, buf)
    selection := document.Find("ul.todo-list li.completed")
    assert.Equal(t, 1, selection.Size())
    assert.Equal(t, "Bar", text(selection.Nodes(0)))
  }

source

Java

  @Test
  void completedItemsGetCompletedClass() {
      var model = new TodoList();
      model.add("Foo");
      model.addCompleted("Bar");
  
      var html = renderTemplate("/index.tmpl", model);
  
      Document document = Jsoup.parse(html, "");
      var selection = document.select("ul.todo-list li.completed");
      assertThat(selection).hasSize(1);
      assertThat(selection.text()).isEqualTo("Bar");
  }

source

This test can be made green by adding this logic to the template:

go

  <ul class="todo-list">
    {{ range .Items }}
      <li class="{{ if .IsCompleted }}completed{{ end }}">
        <div class="view">
          <input class="toggle" type="checkbox">
          <label>{{ .Title }}</label>
          <button class="destroy"></button>
        </div>
      </li>
    {{ end }}
  </ul>

source

Java-jmustache

  <ul class="todo-list">
    {{ #allItems }}
    <li class="{{ #isCompleted }}completed{{ /isCompleted }}">
      <div class="view">
        <input class="toggle" type="checkbox">
        <label>{{ title }}</label>
        <button class="destroy"></button>
      </div>
    </li>
    {{ /allItems }}
  </ul>

source

This way, bit by bit, we can test and add various dynamic features that our template should have.

We will publish this article in installments. Future articles will show how to use parameterization to more easily add new tests, and how to test the behavior of the generated HTML.

To find out when we publish the next issue, subscribe to this site
RSS subscriptionor martin’s feed
Mastodon,
LinkedInor
X (Twitter).


RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments