Skip to content

Commit

Permalink
docs: document view model usage and testing
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h committed Nov 12, 2023
1 parent fe52296 commit b9e33d7
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 0 deletions.
99 changes: 99 additions & 0 deletions docs/docs/04-core-concepts/03-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Testing

To test that data is rendered as expected, there are two main ways to do it:

* Expectation testing - testing that specific expectations are met by the output.
* Snapshot testing - testing that outputs match a pre-written output.

## Expectation testing

Expectation validates that data appears in the output in the right format, and position.

The example at https://github.com/a-h/templ/blob/main/examples/blog/posts_test.go uses the `goquery` library to make assertions on the HTML.

```go
func TestPosts(t *testing.T) {
testPosts := []Post{
{Name: "Name1", Author: "Author1"},
{Name: "Name2", Author: "Author2"},
}
r, w := io.Pipe()
go func() {
posts(testPosts).Render(context.Background(), w)
w.Close()
}()
doc, err := goquery.NewDocumentFromReader(r)
if err != nil {
t.Fatalf("failed to read template: %v", err)
}
// Assert.
// Expect the page title to be set correctly.
expectedTitle := "Posts"
if actualTitle := doc.Find("title").Text(); actualTitle != expectedTitle {
t.Errorf("expected title name %q, got %q", expectedTitle, actualTitle)
}
// Expect the header to be rendered.
if doc.Find(`[data-testid="headerTemplate"]`).Length() == 0 {
t.Error("expected data-testid attribute to be rendered, but it wasn't")
}
// Expect the navigation to be rendered.
if doc.Find(`[data-testid="navTemplate"]`).Length() == 0 {
t.Error("expected nav to be rendered, but it wasn't")
}
// Expect the posts to be rendered.
if doc.Find(`[data-testid="postsTemplate"]`).Length() == 0 {
t.Error("expected posts to be rendered, but it wasn't")
}
// Expect both posts to be rendered.
if actualPostCount := doc.Find(`[data-testid="postsTemplatePost"]`).Length(); actualPostCount != len(testPosts) {
t.Fatalf("expected %d posts to be rendered, found %d", len(testPosts), actualPostCount)
}
// Expect the posts to contain the author name.
doc.Find(`[data-testid="postsTemplatePost"]`).Each(func(index int, sel *goquery.Selection) {
expectedName := testPosts[index].Name
if actualName := sel.Find(`[data-testid="postsTemplatePostName"]`).Text(); actualName != expectedName {
t.Errorf("expected name %q, got %q", actualName, expectedName)
}
expectedAuthor := testPosts[index].Author
if actualAuthor := sel.Find(`[data-testid="postsTemplatePostAuthor"]`).Text(); actualAuthor != expectedAuthor {
t.Errorf("expected author %q, got %q", actualAuthor, expectedAuthor)
}
})
}
```

## Snapshot testing

Snapshot testing is a more broad check. It simply checks that the output hasn't changed since the last time you took a copy of the output.

It relies on manually checking the output to make sure it's correct, and then "locking it in" by using the snapshot.

templ uses this strategy to check for regressions in behaviour between releases, as per https://github.com/a-h/templ/blob/main/generator/test-html-comment/render_test.go

To make it easier to compare the output against the expected HTML, templ uses a HTML formatting library before executing the diff.

```go
package testcomment

import (
_ "embed"
"testing"

"github.com/a-h/templ/generator/htmldiff"
)

//go:embed expected.html
var expected string

func Test(t *testing.T) {
component := render("sample content")

diff, err := htmldiff.Diff(component, expected)
if err != nil {
t.Fatal(err)
}
if diff != "" {
t.Error(diff)
}
}
```
49 changes: 49 additions & 0 deletions docs/docs/04-core-concepts/04-view-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# View models

With templ, you can pass any Go type into your template as parameters, and you can call arbitrary functions.

However, if the parameters of your template don't closely map to what you're displaying to users, you may find yourself calling a lot of functions within your templ files to reshape or adjust data, or to carry out complex repeated string interpolation or URL constructions.

This can make template rendering hard to test, because you need to set up complex data structures in the right way in order to render the HTML. If the template calls APIs or accesses databases from within the templates, it's even harder to test, because then testing your templates becomes an integration test.

A more reliable approach can be to create a "View model" that only contains the fields that you intend to display, and where the data structure closely matches the structure of the visual layout.

```go
package invitesget

type Handler struct {
Invites *InviteService
}

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
invites, err := h.Invites.Get(getUserIDFromContext(r.Context()))
if err != nil {
//TODO: Log error server side.
}
m := NewInviteComponentViewModel(invites, err)
teamInviteComponentModel(m).Render(r.Context(), w)
}

func NewInviteComponentViewModel(invites []models.Invite, err error) (m InviteComponentViewModel) {
m.InviteCount = len(invites)
if err != nil {
m.ErrorMessage = "Failed to load invites, please try again"
}
return m
}


type InviteComponentViewModel struct {
InviteCount int
ErrorMessage string
}

templ teamInviteComponent(model InviteComponentViewModel) {
if model.InviteCount > 0 {
<div>You have { fmt.Sprintf("%d", model.InviteCount) } pending invites</div>
}
if model.ErrorMessage != "" {
<div class="error">{ model.ErrorMessage }</div>
}
}
```

0 comments on commit b9e33d7

Please sign in to comment.