Skip to content
gohacks.dev
Testing

Generate Go Test Cases From JSON Files Stored on Disk

Drive Go table tests from a JSON file in testdata: decode into a case struct, run each as a subtest by name, and add cases without touching Go code.

3 min read
Summarize in AI
Flat illustration of a test report with one failing test, with the JSON logo.

A table test in Go is usually a slice of structs in the test file. That works until the table is large, or until people who are not writing Go want to add cases: a support engineer with a list of inputs that broke last week, an analyst with sample documents, a product owner with edge cases for a pricing rule. For them, a JSON file is a better format than a slice literal.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

total.goGo
package main

type Case struct {
	Name  string `json:"name"`
	Items []int  `json:"items"`
	Want  int    `json:"want"`
}

func Total(items []int) int {
	t := 0
	for _, v := range items {
		t += v
	}
	return t
}

func main() {}
total_test.goGo
package main

import (
	"encoding/json"
	"os"
	"testing"
)

func TestTotalFromJSON(t *testing.T) {
	raw, err := os.ReadFile("testdata/total.json")
	if err != nil {
		t.Fatal(err)
	}
	var cases []Case
	if err := json.Unmarshal(raw, &cases); err != nil {
		t.Fatal(err)
	}
	for _, c := range cases {
		t.Run(c.Name, func(t *testing.T) {
			if got := Total(c.Items); got != c.Want {
				t.Errorf("Total(%v) = %d, want %d", c.Items, got, c.Want)
			}
		})
	}
}
Shell
go test -count=1 -v . 2>&1 | grep -vE "^(=== RUN|=== PAUSE|=== CONT|PASS|ok)" | sed -E "s/ \([0-9.]+s\)//; s#^ +[a-z_]+\.go:[0-9]+: #    #"
Text
    open testdata/total.json: no such file or directory
--- FAIL: TestTotalFromJSON
FAIL
FAIL	example.test/lab	0.232s
FAIL

How it works

The test reads testdata/total.json, decodes it into []Case with encoding/json, and loops. Each case runs as a subtest named from its name, so the output lists them individually and go test -run 'TestTotalFromJSON/negative' runs one. The JSON has four entries, one of them wrong on purpose, and the log shows three passes and one failure: Total([1 1]) = 2, want 3.

The Case struct carries JSON tags, so the file's field names are part of the format. Decode errors fail the test at once with t.Fatal, because a malformed fixture would otherwise run zero cases and pass. A guard worth adding is if len(cases) == 0 { t.Fatal("no cases") }, for the same reason.

What the failure looks like

A failing case prints the case's name in the subtest header, then your message. With plain table tests, people sometimes forget to include the input in the message and end up counting rows. Here the message includes the items and both numbers, and the name identifies the row. Make that a rule for data driven tests: the message must contain enough to reproduce the case without opening the file. When the data is large, print an index or an identifier from the case instead, such as a ticket number in the name.

The flow for fixing a reported bug becomes short. Add a case with the failing input and the correct expected value, run the test and watch that one case fail, fix the code, and watch it pass. The new case stays in the file as a regression test, and the diff is a handful of lines of data.

Good habits

Keep the file next to the test in testdata. Give every case a unique, descriptive name, since it is the failure label. Use DisallowUnknownFields on the decoder so a typo like "wnat" is an error and not a case with a zero expectation. For inputs that are not valid JSON, such as binary data, store files and list their names in the table; the golden files article covers that layout.

The trade-off is that the file has no type checking, no comments and no shared helper values. Go tables win when cases need computed values or function fields. JSON wins when the cases are data. The format also travels: the same file can drive a test in another language, which is useful when two implementations must agree. The decoding side is in the encoding/json reference, and the base pattern is described in table-driven tests. The testing guide shows where it fits.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.