When building server-side applications with Muxt-generated routes, you often want to verify both the
HTTP response (e.g., status codes, headers)
and the HTML/DOM output (e.g., specific elements, text content, or errors).
The domtest
package offers a convenient table-driven approach to do exactly
that—asserting both HTTP and HTML-based outcomes in one cohesive flow.
Below is an example test suite from the blog_test
package, which illustrates how to integrate domtest
with a Muxt
route function named Routes
.
A typical BDD test pattern emerges. Each test case specifies Given
(setup, optional), When
(the request, required),
and Then
(assertions, optional).
By leveraging domtest
’s various assertion helpers, you can check DOM structure and content directly.
Full Example
This is an excerpt from a test. To see the complete code run.
# I haven't actually run this script. It should get the gist across though.
go install golang.org/x/exp/cmd/txtar@latest
go install github.com/maxbrunsfeld/counterfeiter/v6@latest
git clone [email protected]:crhntr/muxt.git
cd muxt
export TEST_TAR="${PWD}/cmd/muxt/testdata/blog.txt"
mkdir -p /tmp/example.com
txtar --extract <"${TEST_TAR}"
go mod tidy
muxt generate
go test -v
package blog_test
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/html/atom"
"github.com/crhntr/dom/domtest"
"github.com/crhntr/dom/spec"
"example.com/blog"
"example.com/blog/internal/fake"
)
func TestBlog(t *testing.T) {
for _, tt := range []domtest.Case[*testing.T, fake.App]{
{
Name: "viewing the home page",
Given: func(t *testing.T, app *fake.App) {
app.ArticleReturns(blog.Article{
Title: "Greetings!",
Content: "Hello, friends!",
Error: nil,
})
},
When: func(t *testing.T) *http.Request {
return httptest.NewRequest(http.MethodGet, "/article/1", nil)
},
Then: domtest.Document(func(t *testing.T, document spec.Document, app *fake.App) {
require.Equal(t, 1, app.ArticleArgsForCall(0))
if heading := document.QuerySelector("h1"); assert.NotNil(t, heading) {
require.Equal(t, "Greetings!", heading.TextContent())
}
if content := document.QuerySelector("p"); assert.NotNil(t, content) {
require.Equal(t, "Hello, friends!", content.TextContent())
}
}),
},
{
Name: "the page has an error",
Given: func(t *testing.T, app *fake.App) {
app.ArticleReturns(blog.Article{
Error: fmt.Errorf("lemon"),
})
},
When: func(t *testing.T) *http.Request {
return httptest.NewRequest(http.MethodGet, "/article/1", nil)
},
Then: domtest.QuerySelector("#error-message", func(t *testing.T, msg spec.Element, app *fake.App) {
require.Equal(t, "lemon", msg.TextContent())
}),
},
{
Name: "the page has an error and is requested by HTMX",
Given: func(t *testing.T, app *fake.App) {
app.ArticleReturns(blog.Article{
Error: fmt.Errorf("lemon"),
})
},
When: func(t *testing.T) *http.Request {
req := httptest.NewRequest(http.MethodGet, "/article/1", nil)
req.Header.Set("HX-Request", "true")
return req
},
Then: domtest.Fragment(atom.Body, func(t *testing.T, fragment spec.DocumentFragment, app *fake.App) {
el := fragment.FirstElementChild()
require.Equal(t, "lemon", el.TextContent())
require.Equal(t, "*errors.errorString", el.GetAttribute("data-type"))
}),
},
{
Name: "when the id is not an integer",
When: func(t *testing.T) *http.Request {
return httptest.NewRequest(http.MethodGet, "/article/banana", nil)
},
Then: func(t *testing.T, res *http.Response, f *fake.App) {
require.Equal(t, http.StatusBadRequest, res.StatusCode)
},
},
} {
t.Run(tt.Name, tt.Run(func(app *fake.App) http.Handler {
mux := http.NewServeMux()
blog.Routes(mux, app)
return mux
}))
}
}
-
domtest.Case[*testing.T, fake.App]
Each case is parameterized by the test context (*testing.T
) and your fake receiver type (fake.App
). -
Given func(t *testing.T, app *fake.App)
Set up initial conditions—e.g., “ArticleReturns” to specify what happens when the route callsapp.Article(id)
. -
When func(t *testing.T) *http.Request
Creates the incoming request for this scenario (method, path, optional headers/body). -
Then ...
A function to assert the result, either at the HTTP level or the DOM level. This can be:domtest.Document(...)
for a full HTML doc,domtest.QuerySelector(...)
for a specific element,domtest.Fragment(...)
for partial responses, or- a custom function that checks
response.StatusCode
directly.
Inside each Then
block, you can use require
or assert
from stretchr/testify
to fail the test if the expected DOM elements or status codes aren’t present.
The final closure passed into Run is for you to call the muxt generated routes
function.
It receives the fake receiver as a parameter.
Then: domtest.Document(func(t *testing.T, document spec.Document, app *fake.App) {
require.Equal(t, 1, app.ArticleArgsForCall(0)) // Did we call Article(1)?
heading := document.QuerySelector("h1")
if assert.NotNil(t, heading) {
require.Equal(t, "Greetings!", heading.TextContent())
}
content := document.QuerySelector("p")
if assert.NotNil(t, content) {
require.Equal(t, "Hello, friends!", content.TextContent())
}
}),
- The test ensures
app.Article(1)
was called. - Looks up
<h1>
and<p>
tags and verifies text content matches the expectation.
{
Name: "the page has an error",
Given: func(t *testing.T, app *fake.App) {
app.ArticleReturns(blog.Article{
Error: fmt.Errorf("lemon"),
})
},
When: func(t *testing.T) *http.Request {
return httptest.NewRequest(http.MethodGet, "/article/1", nil)
},
Then: domtest.QuerySelector("#error-message", func(t *testing.T, msg spec.Element, app *fake.App) {
require.Equal(t, "lemon", msg.TextContent())
}),
},
- Sets up the domain method to return an error.
- Uses
domtest.QuerySelector("#error-message", ...)
to confirm the<div id="error-message">
or similar element contains the string"lemon"
.
Example: Partial Responses HTMX
Although the Document parser will allow incomplete documents, you may want to test document fragments.
{
Name: "the page has an error and is requested by HTMX",
Given: func(t *testing.T, app *fake.App) {
app.ArticleReturns(blog.Article{Error: fmt.Errorf("lemon")})
},
When: func(t *testing.T) *http.Request {
req := httptest.NewRequest(http.MethodGet, "/article/1", nil)
req.Header.Set("HX-Request", "true")
return req
},
Then: domtest.Fragment(atom.Body, func(t *testing.T, fragment spec.DocumentFragment, app *fake.App) {
el := fragment.FirstElementChild()
require.Equal(t, "lemon", el.TextContent())
require.Equal(t, "*errors.errorString", el.GetAttribute("data-type"))
}),
},
- Simulates an HTMX request by adding
HX-Request: true
. - Uses
domtest.Fragment(atom.Body, ...)
to parse only a<body>
snippet or partial, checking content.
-
One-Stop Testing
- Tests your real
Routes(mux, fakes)
in a black-box manner: if Muxt or your domain logic break, the test fails.
- Tests your real
-
Domain + Presentation
- Check domain calls (e.g.,
ArticleArgsForCall(0) == 1
) and the rendered DOM (the<h1>
or error messages).
- Check domain calls (e.g.,
-
Minimal Overhead
domtest
sets up a structure that’s easy to read, maintain, and expand. Adding new test cases or scenarios is straightforward.
-
Supports TDD/BDD
- The table-driven approach with
Given
,When
, andThen
aligns naturally with Behavior-Driven Development or Extreme Programming’s quick feedback loop.
- The table-driven approach with
-
Name Scenarios Clearly I did not do a great job of this in my example (e.g., “viewing the home page,” “the page has an error,” “when the id is not an integer.”)
-
Use Mocking Tools
- For more complex domain interactions, consider a mocking library like counterfeiter, generating stubs for your Muxt receiver methods.
-
Keep Tests Atomic
- Each scenario should test one major idea: e.g., “user not logged in -> unauthorized,” or “invalid input -> show error.” Avoid piling too many steps into a single test.
-
Combine with Muxt’s Type Checking
- If you’re using Muxt’s static type check feature, you’ll get extra assurance that your templates, route parameters, and domain method signatures align correctly before even hitting these tests.
(this article was mostly generated using an LLM model)