-
-
Notifications
You must be signed in to change notification settings - Fork 288
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: document view model usage and testing
- Loading branch information
Showing
2 changed files
with
148 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} | ||
} | ||
``` |