Skip to content

Test without launching a browser

This guide shows how to use WithOpener to inject a fake opener in tests, so your test suite never spawns a real browser or mail client — and how to assert on the URL that would have been opened.

The problem

By default, OpenURL delegates to the OS URL handler. In a test, that would pop a real browser window (or fail on a headless CI machine). You want to exercise your code's URL-opening path and check what it produced, without any side effect on the machine running the tests.

Inject a fake with WithOpener

OpenURL accepts options, and WithOpener replaces the default OS opener with any func(rawURL string) error you supply. Point it at a closure that records the URL instead of opening it:

func TestOpensDocsURL(t *testing.T) {
    var opened string

    err := browser.OpenURL(context.Background(), "https://example.com/docs",
        browser.WithOpener(func(rawURL string) error {
            opened = rawURL
            return nil
        }),
    )
    if err != nil {
        t.Fatalf("OpenURL returned an error: %v", err)
    }

    if opened != "https://example.com/docs" {
        t.Errorf("opened %q, want the docs URL", opened)
    }
}

The fake receives the URL only after validation has passed, so a captured value confirms the URL cleared the scheme allowlist, the length bound, and the control-character check.

Validation still runs — the fake is never called on a reject

WithOpener does not bypass the gate. If the URL is rejected, OpenURL returns the sentinel error and your opener is never invoked — so you can assert that a bad URL never reached the "browser":

func TestRejectsDisallowedScheme(t *testing.T) {
    called := false

    err := browser.OpenURL(context.Background(), "javascript:alert(1)",
        browser.WithOpener(func(string) error {
            called = true
            return nil
        }),
    )

    if !errors.Is(err, browser.ErrDisallowedScheme) {
        t.Fatalf("want ErrDisallowedScheme, got %v", err)
    }
    if called {
        t.Error("opener was called for a disallowed scheme")
    }
}

Simulate an OS-opener failure

Return an error from the fake to exercise your code's handling of a failed open (for example, a headless host with no browser). OpenURL wraps it, so match on your own error, not on a sentinel:

sentinel := errors.New("no browser available")

err := browser.OpenURL(context.Background(), "https://example.com",
    browser.WithOpener(func(string) error {
        return sentinel
    }),
)

if !errors.Is(err, sentinel) {
    t.Fatalf("want the opener error to propagate, got %v", err)
}

A nil opener is ignored, not a way to disable opening

WithOpener(nil) does not clear the opener and does not switch opening off. The nil is discarded and whatever is already configured stays in place — the default OS opener if no earlier option set one, or an earlier WithOpener's function if one did:

browser.OpenURL(ctx, u,
    browser.WithOpener(fake.Open),
    browser.WithOpener(nil),  // ignored; fake.Open is still used
)

browser.OpenURL(ctx, u,
    browser.WithOpener(nil),  // ignored; the real OS opener is still used
)

The second call will launch a browser. If a test needs no browser to open, pass a real no-op closure — never nil.

Where two non-nil openers are supplied, the last one wins.

The fake applies to one call only

There is no global opener to install or reset. WithOpener configures the single OpenURL call it is passed to, which is what makes it safe under t.Parallel(): two tests can each intercept their own calls with no shared mutable state and nothing to restore in a cleanup function.

The corollary is that every call site you want to intercept needs its own WithOpener. If your code under test calls OpenURL internally, thread the option through — for example by having the function under test take a ...browser.Option and pass it on.

See also