Go test reporting
go test gives you ok,
FAIL, and a scrollback buffer that ends when the job does.
Qualflare has a
native Go reporter
that turns those runs into hosted, historical reporting: every
t.Run subtest as its own case, AI clustering of failures by root
cause, flaky scores from run history, and every sharded CI job merged into one launch.
Go’s native reporting options, explained
Go ships a test runner, not a reporter. There is no plugin API and no per-test hook — which is
why the whole ecosystem is built on reading go test’s
output rather than hooking into it. Four flags cover everything the toolchain itself offers:
go test ./... # ok / FAIL per package, nothing more
go test -v ./... # === RUN / --- PASS per test, unstructured
go test -json ./... # test2json events, one JSON object per line
go test -cover ./... # coverage percentages, a different question go test -json. The only structured output Go produces. It emits one JSON object per line via test2json —run,output,pass/failevents with package and test names attached. Everything below is a consumer of this stream.- gotestsum. Wraps
go test -json, prints a readable summary in one of several console formats, and will write a JUnit XML file with--junitfile. The most common way Go suites end up with XML in CI. - go-junit-report. A pure converter: pipe
go test -vtext, orgo test -jsonwith-parser gojson, and get JUnit XML out. Its own docs tell you to redirect stderr — for a reason we’ll come back to. - tparse, gotestdox, richgo. Terminal formatters — tables, sentence-cased test names, colour. They change how a run reads while you watch it; they produce no artifact to collect afterward.
go test -cover. Worth being precise about: coverage is the percentage of your code the suite executed, which is a different question from what passed and what failed.-coverprofileplusgo tool cover -htmlis a separate report from everything else here.
If all you need is XML for a CI plugin, either of these is a fine one-liner:
# gotestsum: pretty console output and a JUnit file in one pass
gotestsum --junitfile unit-tests.xml
# go-junit-report: converts the stream after the fact
go test -json 2>&1 | go-junit-report -parser gojson > report.xml Why go test output isn’t enough
All of it is per-run and local. Nothing accumulates: each run’s console output or XML file is
independent, so “has TestCheckout/expired_card been getting
flakier this month” is not a question any of them can answer. And the JUnit conversions lose
information on the way — Go’s stream distinguishes outcomes that the XML schema simply has no
place for, so a skipped test, a panicking test and a package that timed out arrive looking more
alike than they were.
For history you need something that stores results over time and analyzes them.
Send Go test results to Qualflare
The native reporter replaces go test at the front of your
command. It requires no change to your test code:
# The native reporter: wraps go test, writes a report directory
go install github.com/Qualflare/qualflare-go/cmd/qualflare-go@latest
qualflare-go ./... qualflare-go ./... is shorthand for
qualflare-go -- go test ./...; it adds
-json for you and passes through any flag
go test accepts. It makes no network calls — it writes a
report directory, which the CLI uploads:
# Upload the directory — one launch, however many shards wrote into it
qf my-project collect ./qualflare-results In CI that’s one changed line and one added step (GitHub Actions shown — GitLab CI, Bitbucket Pipelines, and Jenkins work the same way). Authenticate the CLI once with your Qualflare access token, stored as a CI secret — see the CLI docs.
# .github/workflows/tests.yml
- name: Run tests
run: qualflare-go -- go test -race ./...
- name: Upload results to Qualflare
if: always() # upload even when tests fail — that's the point
run: qf my-project collect ./qualflare-results
Already producing JUnit XML from gotestsum or go-junit-report? That works too —
qf my-project collect report.xml --format junit, or
--format golang for a raw
go test -json file. You get status, duration and name;
the native reporter is what adds steps, attachments and metadata.
What you get on top of go test
- Subtests as first-class cases. Every
t.Runrow gets its own status, duration and history rather than being collapsed into its parent. - AI failure clustering. When one broken fixture or a downed dependency takes out 40 tests, Qualflare groups them by root cause — so you fix one thing instead of reading 40 stack traces.
- Flaky detection from history. Each test is scored from its pass/fail record across runs. Go has no retry mechanism to read, so history is not merely the better method here — it is the only one.
- Sharded CI aggregation. Point every shard at the same output directory and collect once; the CLI merges them into a single launch instead of one report per worker.
- Build failures that stay visible. The wrapper watches the exit code and stderr, not just the JSON stream — see below for why that distinction is load-bearing.
- History, trends & defects. Pass rate, slowest tests, and flakiness over time across branches — plus a defect you can open straight from a failing run.
Raw go test output vs Qualflare
| go test / gotestsum | Qualflare | |
|---|---|---|
| History across CI runs | — | Yes |
| Merges sharded CI jobs into one run | — | Yes |
| AI failure clustering (root cause) | — | Yes |
| Flaky scoring over time | — | Yes |
| Steps, attachments & labels on a test | — | Yes |
| Local, zero-setup, offline | Yes | — |
Complementary: keep go test and your terminal formatter for
local runs, add Qualflare for hosted, historical CI observability.
Get AI analysis on your Go test runs
Start free — swap in qualflare-go, run qf collect, and get your first AI analysis in minutes.
Qualflare works the same with pytest, Playwright, Cypress, Jest, JUnit and 20+ more frameworks. Testing a mobile app too? See our Android (Espresso), iOS (XCTest), and Maestro guides. Weighing tools? See how it compares to other test management platforms, or browse all framework reporting guides. Every reporter is open source.
Subtests, build failures, and the test cache
Three Go behaviors shape what your reports look like. First,
table-driven tests fan out. The Go idiom is one test function
looping over a slice of cases, and each t.Run is reported
as its own case with the row name in its ID. One function can legitimately be 200 rows in your
report — which is the point, because a flaky row is visible instead of being averaged into
its parent:
// Table-driven: each row is its own case in the report, with its own
// status, duration and history — not a step inside TestLogin.
func TestLogin(t *testing.T) {
for _, tc := range []struct{ name, role string }{
{"admin", "admin"}, {"guest", "guest"},
} {
t.Run(tc.name, func(t *testing.T) { ... })
}
}
Second, a package that doesn’t compile can be invisible. On Go
1.21 and 1.23, a build failure produces no failure event in the
go test -json stream at all: the stream is entirely
green while go test exits 1, and the compiler error goes
only to stderr. That is why qualflare-go wraps the command
instead of reading a pipe — a pipe sees neither the exit code (without
set -o pipefail) nor stderr, and would happily upload a
green launch for a broken build. It is also why go-junit-report’s own documentation tells you to
redirect stderr into it.
Third, Go caches passing results, so a “run” may not have run
anything. A cached package prints (cached) and produces
no timing — which quietly flatters your duration trends and hides flakiness, because a test that
never executed cannot fail. -count=1 is the idiomatic way
to force a real run, and it’s worth having in CI:
$ go test ./...
ok m 0.414s
$ go test ./...
ok m (cached) # nothing ran; the result was replayed from disk
$ go test -count=1 ./...
ok m 0.235s # -count=1 is the idiomatic cache buster
Relatedly, Go has no built-in retry. There is no
--retries flag and no rerun plugin to read, so in-run
attempt data only exists under -count=N, where a test
genuinely runs more than once. Flakiness scoring therefore comes from history across launches —
which is the more accurate method anyway, since in-run retries tend to mask flakiness rather than
expose it.
Optional: labels, steps and attachments
Reporting works with no code change at all. If you want more than a name and a status, the module
adds an author-facing API. It has zero external dependencies, so importing it adds nothing to your
go.mod graph, and nothing in it can fail your test — no
function returns an error or panics, and calls are inert when no reporter is listening:
import "github.com/Qualflare/qualflare-go"
func TestCheckout(t *testing.T) {
qualflare.Label(t, "feature", "checkout")
qualflare.Tag(t, "smoke")
qualflare.Link(t, "https://example.com/issue/42", qualflare.LinkIssue, "QF-42")
qualflare.Step(t, "add to cart", func() {
qualflare.Parameter(t, "sku", "widget")
qualflare.MaskedParameter(t, "token")
})
}
Every call takes the testing.TB first. Go has no
goroutine-local storage, so the explicit t is the only
reliable handle on which test is running — and it makes metadata outside a test a compile error
rather than a runtime rule. Full reference in the
metadata API docs.
Frequently asked questions
How do I send go test results to Qualflare?
Install the native reporter with go install github.com/Qualflare/qualflare-go/cmd/qualflare-go@latest, then run qualflare-go ./... in place of go test ./.... It writes a report directory and makes no network calls; qf my-project collect ./qualflare-results uploads it. If you already have JUnit XML from gotestsum or go-junit-report, you can upload that instead with qf my-project collect report.xml --format junit — you just get less detail.
Are Go subtests reported as separate tests?
Yes. Every t.Run subtest becomes its own case with its own status, duration and history, so a table-driven test with 200 rows is 200 rows in the report. That matters because flakiness lives at the row level: if only one parameter combination is intermittent, folding the table into a single case averages that signal away.
Does Qualflare detect flaky Go tests?
Yes — by scoring each test’s pass/fail record across runs, which is the accurate method and the only one available in Go. Go has no built-in retry mechanism, so there is no in-run rerun data to read; the exception is go test -count=N, where a test genuinely runs more than once and those attempts are recorded. Flakiness is never guessed from separate go test invocations.
Does it work with parallel tests and sharded CI jobs?
Yes. t.Parallel() needs no configuration. For sharded CI, point every shard at the same --output-dir and run qf collect once at the end: each invocation writes a uniquely named file, so shards never overwrite each other, and the CLI merges every file in the directory into one launch. Shards derive a shared run id from the CI build automatically, or you can pass --run-id.
What happens when a Go package fails to compile?
This is the reason to use the wrapper rather than a pipe. On Go 1.21 and 1.23 a build failure produces no failure event in the go test -json stream at all — the stream is entirely green while go test exits 1, and the compiler error goes only to stderr. qualflare-go -- go test ./... sees both the exit code and stderr, so a broken build is reported as a broken build instead of a green launch.
Do I have to change my test code?
No. The reporter is a wrapper around go test and reports every test without any code change. Importing the library is optional and only adds metadata — labels, tags, links, steps, attachments and parameters. It has zero external dependencies, so it adds nothing to your go.mod graph, and nothing in the API can fail your test.
Setup reflects qualflare-go and the Qualflare CLI (docs.qualflare.com) as of September 2026. The build-failure and test-cache behaviour described above was measured on Go 1.21, 1.23 and 1.26, not inferred from documentation. Written by İbrahim Süren, Qualflare.