Gost-DOM
The go-to solution for a web application TDD workflowGost-DOM is a headless browser written in Go to help build provide a fast feedback loop of the HTTP for web application development in Go. It features a DOM implemented in Go and a V8 JavaScript engine exposing the DOM to client-scripting; as well as a subset of the browser APIs.
Gost-DOM is specifically written with HTMX in mind.
How it works
Gost-DOM can eliminate the overhead TCP transport layer by consuming the HTTP handler directly. As well as improving test performance, it eliminates all complexity of managing server startup and shutdown in test code; while providing the ability for isolated parallel tests of the web application.
You can construct an instance of the
Browser
passing an
http.Handler
as argument.1
Any window opened from the browser will be instantiated with the default script engine, currently V8.2 The window provides a subset3 of the DOM, allowing developers to write the tests using a familiar syntax, the DOM.
// server.go
var MyRootHttpServer = http.DefaultServeMux
func init() {
http.HandleFunc("GET /", func(
w http.ResponseWriter,
r *http.Request) {
w.Write([]byte(html))
})
}
const html = `<body>
<h1>My title</h1>
<p>Lorem ipsum</p>
</body>`
// server.go
var MyRootHttpServer = http.DefaultServeMux
func init() {
http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("<body><h1>My title</h1><p>Lorem ipsum</p></body>"))
})
}
// server_test.go
func TestMyServer(t *testing.T) {
browser := browser.New(MyRootHttpServer)
// Ignore errors in this example
win, _ := browser.Open("/")
pageTitle, _ := win
.Document()
.QuerySelector("h1")
assert.Equal(t,
"My title",
pageTitle.TextContent())
}
// server_test.go
func TestMyServer(t *testing.T) {
browser := browser.New(MyRootHttpServer)
// Ignore errors in this example
win, _ := browser.Open("/")
pageTitle, _ := win.Document().QuerySelector("h1")
assert.Equal(t, "My title", pageTitle.TextContent())
}
Selling points
Gost-DOM is an efficient tool for testing, partially because it allows bypassing
the TCP stack and consume the ServeHTTP
function directly.
- Speed - By eliminating the TCP stack, fetching resources are normal function calls.
- No hassle managing TCP ports - Automating startup/shutdown of a test-managed server is often a painpoint for web application testing. With Gost-DOM, this layer doesn't exist.
- Isolation - Each
Browser
instance has it’s own V8 engine; they cannot interfere with one another. - Parallelism - Isolation facilitates parallel test execution, as long as your code supports parallel test runs.
- Mocking - With Gost-DOM you treat the SUT as Go-code like any other test; facilitating replacing real dependencies with test doubles.
- Time travel -
Gost-DOM has a virtual clock controllable by test code, affecting the behaviour of
setTimeout
andsetInterval
. Your tests control the passing of time, e.g. for throttled or debounced behaviour, supporting:- Advance time sufficiently - Ensure all effects have executed by advancing time several seconds.
- Advance time precisely - Advance time precisely to verify the exact throttling behaviour.
Shaman - Helper library for more expressive tests
An unrelease side project, Shaman, is in the works. This provides helpers on top of Gost allowing test cases to be more expressive, and simulate user behaviour.
func TestMyServer(t *testing.T) {
window := OpenWindow()
scope := scope.New(window.Document())
form := scope.SubScope(
scope.ByRole(ariarole.Form))
form.Find(
ByRole(ariarole.TextBox),
ByName("Email"),
).Type("smith@example.com")
form.Find(
ByRole(ariarole.Button),
ByName("Reset password"),
).Click()
// Assert something happened!
}
func TestMyServer(t *testing.T) {
window := OpenWindow()
scope := scope.New(window.Document())
form := scope.SubScope(scope.ByRole(ariarole.Form))
form.Find(ByRole(ariarole.TextBox), ByName("Email")).Type("smith@example.com")
form.Find(ByRole(ariarole.Button), ByName("Reset password")).Click()
// Assert something happened!
}
Locate elements like users do
When a user interacts with a page, the locate the elements to interact with by contextual information for the elements.
A visual user relies on visual affinity of elements to bring context, e.g. the “text” next to an input field signifies what to type here.
A user relying on a screen reader depends on proper semantic meaning in the codes to being the same context. They rely on the accessibility name of the element to bring the same meaning.
In both cases, the user sees input fields with names. Often, test code relies on
implementation details, such as element attributes like id
or data-testid
.
Shaman promotes writing tests that express how a user interact with the
application, yieling benefits:
- Promote accessible design by default.
- Makes the tests resilient to code refactoring.
- Makes the tests resilient to changes to the visual design.
Shaman is sponsors only
At the time of the first pre-release, Shaman will be an exclusive to sponsors at the appropriate sponsorship tiers; It might eventually be made public.
I hope to make shaman publically available in the future, but I need to provide some boon for to encourage sponsorships.
-
You can create an instance of
html.Window
directly, and can potentially yield better performance in the current release. But the the browser is it brings sensible defaults, and it’s less likely to undergo a breaking change. ↩ -
Work is in progress to support goja as a pure Go alternative to V8, eliminating the need for CGo. ↩
-
While we may work towards full compliance, the priority is writing a tool for testing modern web applications. Adding support for old deprecated standards is low on the priority list. ↩