A test fixture is part of the test’s explanation. If a reviewer cannot tell what a fixture represents, the test has lost an important part of its documentation.

The clearest example is binary data written directly into a Go string:

1w.Write([]byte("PK\\x03\\x04\\x14\\x00\\b\\x00..."))

It may be technically correct, but it is effectively opaque. A reviewer cannot reasonably inspect the archive, update it safely, or tell whether a one-byte change is intentional.

My default is now: do not inline substantial binary protocol or archive data in a test body. Give it a name and a representation that matches what the test cares about.

If exact bytes matter, use a named embedded fixture

When the precise encoded bytes are the contract, put the fixture in testdata and embed it:

1//go:embed testdata/module-v1.0.0.zip
2var moduleV100Zip []byte

The test becomes descriptive:

1proxy := newProxy(t, map[string]response{
2    "/example/module/@v/v1.0.0.zip": {
3        ContentType: "application/zip",
4        Body:        moduleV100Zip,
5    },
6})

The binary file can be inspected with ordinary archive tooling, replaced deliberately, and reused by more than one case.

go:embed is especially useful here because the test no longer depends on its working directory and does not need to copy the fixture to disk merely to read it.

If archive structure matters, generate the bytes from readable input

Often the exact ZIP encoding does not matter. The test only needs a valid archive containing particular files.

In that case, a builder is better than committing a binary:

1zipData := mustZip(t, map[string][]byte{
2    "example/module@v1.0.0/go.mod": []byte(`module example/module
3go 1.22
4`),
5    "example/module@v1.0.0/module.go": []byte("package module\n"),
6})

This is readable in code review and keeps the archive in memory.

For larger trees, a txtar fixture can make the source even clearer:

1-- example/module@v1.0.0/go.mod --
2module example/module
3go 1.22
4-- example/module@v1.0.0/module.go --
5package module

The test helper can turn that readable tree into whatever archive format the protocol needs.

Choose representation according to the assertion

A useful rule is:

What the test cares aboutFixture representation
exact binary bytesnamed embedded binary fixture
files contained in an archivereadable map or txtar, then build in memory
path/tree discoveryfstest.MapFS
one small text request/responseinline string or byte slice
many protocol responsesdeclarative response table plus embedded/text fixtures

The fixture should make the important property obvious and hide incidental encoding detail.

Large HTTP switches are another form of opaque fixture setup

This works for a tiny test server:

1switch r.URL.Path {
2case "/v1/info":
3    w.Write(info)
4case "/v1/mod":
5    w.Write(mod)
6case "/v1/archive":
7    w.Write(archive)
8}

It becomes difficult to read when a test adds many modules, versions, status cases, headers, and alternate responses.

Prefer a small declarative server helper:

 1type response struct {
 2    Status      int
 3    ContentType string
 4    Body        []byte
 5}
 6
 7func newFixtureServer(t testing.TB, routes map[string]response) *httptest.Server {
 8    t.Helper()
 9    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
10        route, ok := routes[r.URL.Path]
11        if !ok {
12            http.NotFound(w, r)
13            return
14        }
15        if route.ContentType != "" {
16            w.Header().Set("Content-Type", route.ContentType)
17        }
18        status := route.Status
19        if status == 0 {
20            status = http.StatusOK
21        }
22        w.WriteHeader(status)
23        _, _ = w.Write(route.Body)
24    }))
25}

Then the test describes the protocol fixture rather than implementing a miniature server inline:

1server := newFixtureServer(t, map[string]response{
2    "/example/module/@v/v1.0.0.info": {Body: infoData},
3    "/example/module/@v/v1.0.0.mod":  {Body: modData},
4    "/example/module/@v/v1.0.0.zip":  {ContentType: "application/zip", Body: zipData},
5})

If request validation matters, make that part explicit too: record requests, declare expected methods or headers, and compare the recorded requests after the operation.

Embed a fixture tree when the protocol is mostly static data

A larger fake service can use a whole embedded directory:

1//go:embed testdata/proxy
2var proxyFixtureFS embed.FS

A helper can map request paths to files below that root. This keeps protocol data out of Go control flow while still allowing tests to override individual responses for failures.

The same principle applies to JSON APIs, HTML fixtures, package registries, mail messages, and generated source corpora.

Do not make binary fixtures mysterious

An embedded binary fixture should have provenance.

Prefer at least one of:

  • a nearby readable source fixture from which it can be regenerated;
  • a small generator helper or command;
  • a comment explaining what property makes the binary special;
  • a deterministic generation test if exact bytes are expected to remain stable.

Otherwise an opaque file merely moves the mystery from a Go literal into testdata.

Standard practice

For test fixtures:

  • never inline large escaped binary blobs in a test body;
  • use go:embed for named exact-byte fixtures when encoded bytes are important;
  • generate archives in memory from readable maps or txtar when archive contents are what matter;
  • prefer declarative HTTP response tables over large path switches;
  • keep static protocol data in fixture files rather than control flow;
  • make fixture provenance or regeneration obvious when binary bytes are checked in.

A good fixture lets the reviewer understand the scenario before running the test.