skip to content

What is testing/fstest.MapFS, and how does a test use it to supply files without touching disk?

level: middleimportance: should knowfreq 34%

answer

  1. a map literal is the whole filesystem
  2. keys are unrooted slash-separated names
  3. values carry the bytes and the metadata
  4. os.DirFS is the production twin
  5. the seam is the parameter type

basics

~20 s

fstest.MapFS is a map from slash-separated file names to *fstest.MapFile that implements fs.FS. A test declares the whole tree as a map literal and passes it wherever the code takes an fs.FS, so nothing is written to disk.

solid answer

~40 s

`fstest.MapFS` is declared as `map[string]*fstest.MapFile`, and it implements `fs.FS`. Each key is an unrooted, slash-separated name such as `"reports/2026-03-08.log"`; each value carries the file's `Data []byte` plus optional `Mode`, `ModTime` and `Sys`. Parent directories are synthesised from the names, so `"reports"` becomes listable without an entry of its own. Because MapFS also implements the common read helpers, `fs.ReadFile` and `fs.WalkDir` work against it unchanged. The precondition is the seam: the function under test must accept an `fs.FS` rather than a directory string. Production then passes `os.DirFS(root)` and the test passes the map literal. The fixture lives in the test source where a reviewer can read it, the test is fast, and there is nothing to clean up.

code

go · 12 lines
go
fsys := fstest.MapFS{
	"reports/2026-03-07.log": {Data: []byte("ok 1\n")},
	"reports/2026-03-08.log": {Data: []byte("ok 2\n")},
}

b, err := fs.ReadFile(fsys, "reports/2026-03-08.log")
if err != nil {
	t.Fatal(err)
}
if string(b) != "ok 2\n" {
	t.Errorf("contents = %q", b)
}

go deeper

for a junior

Be able to write the literal from memory: a quoted slash-separated file name as the key, and a MapFile whose Data field holds the contents as a byte slice.

for a middle

Explain that MapFS implements fs.FS so fs.ReadFile and fs.WalkDir work against it unchanged, that parent directories are synthesised, and how the names differ from operating-system paths.

for a senior

Show the refactor that creates the seam — parameter typed fs.FS, os.DirFS in production — and be able to say when a committed testdata tree is the better fixture instead.

for a principal

Weigh putting an fs.FS parameter in an API other teams import: it fixes read-only access for every consumer and is hard to widen later, so decide whether the testability belongs in the exported surface or behind it.

## The shape `fstest.MapFS` in the standard library's `testing/fstest` package is nothing more than ```go type MapFS map[string]*MapFile ``` with methods hung on it so that it satisfies `fs.FS`. A `MapFile` holds the content and the metadata: ```go type MapFile struct { Data []byte Mode fs.FileMode ModTime time.Time Sys any } ``` A whole test fixture is therefore a composite literal, and because the map's element type is a pointer the `&MapFile` may be elided: ```go fsys := fstest.MapFS{ "reports/2026-03-07.log": {Data: []byte("ok 1\n")}, "reports/2026-03-08.log": {Data: []byte("ok 2\n")}, } ``` ## The naming rules that trip people up Keys are file names as `io/fs` understands them: forward slashes on every platform, no leading slash, no `./` prefix, no drive letters. `"/reports/a.log"` and `"reports\\a.log"` are not paths that resolve to the same file — they are simply different, invalid names, and lookups for `"reports/a.log"` will miss them. The single name `"."` denotes the root. You do not have to list directories. MapFS synthesises the parents implied by the file names, so the fixture above makes `"reports"` a directory you can list. Add an explicit entry only when you need a particular mode or modification time on the directory itself, in which case set `Mode` with `fs.ModeDir`. ## Why it substitutes cleanly MapFS is not only an `fs.FS` with an `Open` method; it also implements the read helpers the standard library reaches for, so package-level functions such as `fs.ReadFile(fsys, name)` and `fs.WalkDir(fsys, ".", fn)` operate on it exactly as they would on a real directory tree. Code written against those helpers does not know or care which implementation it was handed. ## The seam is the parameter, not the fixture MapFS is only usable if the function under test takes a filesystem rather than a location. Compare: ```go func Summarise(dir string) (Report, error) // no seam: it will open real paths func Summarise(fsys fs.FS) (Report, error) // a seam: any fs.FS will do ``` With the second signature, production calls `Summarise(os.DirFS("/var/log/reports"))` and the test calls `Summarise(fsys)` with the map literal. `os.DirFS` is the production twin of MapFS: it turns a real directory into an `fs.FS` rooted there. Changing the parameter type is usually a small edit inside the function — replacing direct file opens with `fs.ReadFile` and `fs.WalkDir` — and it is the whole of the work. If you cannot change the signature (the code shells out, or it is a third component you do not own), MapFS cannot help and a real directory from `t.TempDir` is the fixture. ## What MapFS buys over a testdata directory Both work; they trade different things. - **Visibility.** The fixture is in the test function, three lines above the assertion, instead of in a `testdata` tree a reader has to open in another window. - **Speed and hermeticity.** No file creation, no cleanup, no leftover state from a failed run, no permissions or path-length quirks between operating systems. - **Precision.** Setting `ModTime` or an unusual `Mode` is a struct field rather than a system call, which makes it easy to test code that sorts by modification time or skips entries by mode. A `testdata` directory read through `os.DirFS` still wins when the fixture is genuinely large, binary, or shared between several tests, or when it is a real artefact you want checked into the repository as-is. ## What it cannot do MapFS is **read-only**. `io/fs` has no create, write, rename or remove operation, so any code path that produces files needs a different fixture. Mutating the map between calls is possible in principle, but treat it as fixture setup rather than as a filesystem operation the code under test performs. It is also **not a path**. There is no string you can hand to another process, to `os.Open`, or to a library that insists on a directory name. ## Checking a filesystem you wrote yourself The same package offers `fstest.TestFS(fsys fs.FS, expected ...string) error`, which walks an implementation, exercises its read operations and reports where it deviates from what `io/fs` requires, verifying that the named files are present. That is the tool for the other direction: not faking a filesystem for your code, but proving that a filesystem implementation you wrote behaves like one.

  • What must the function's signature look like before fstest.MapFS is usable at all?
    It has to accept an fs.FS (or a narrower read interface) instead of a directory string, and read through helpers such as fs.ReadFile and fs.WalkDir rather than opening paths itself. Production then supplies os.DirFS(root) and the test supplies the map literal. A function that takes a string and opens real paths has no seam to substitute at.
  • Do you have to spell out every directory entry in a MapFS literal?
    No. Parent directories implied by the file names are synthesised, so an entry for "reports/2026-03-08.log" is enough to make "reports" openable and listable. You add an explicit directory entry only when the test cares about that directory's own mode or modification time.
  • How would you check that a filesystem implementation you wrote yourself behaves correctly?
    Run fstest.TestFS against it, passing the names you expect to find. It walks the tree, exercises the read operations and reports where the implementation deviates from what io/fs requires. That is the reverse use of the package: proving your fs.FS is well behaved rather than faking one for other code.
  • When is a testdata directory read through os.DirFS the better fixture?
    When the input is large, binary, or a real artefact you want committed as-is, or when several tests share it. MapFS wins when the fixture is small enough to read inside the test, when you want to set ModTime or Mode precisely, and when you want no files created at all.

saying these in an interview costs you the question

  • Writes MapFS keys with a leading slash or a dot prefix
  • Uses backslashes in MapFS keys on Windows
  • Believes every parent directory needs its own map entry
  • Thinks the code under test can create files in a MapFS
  • Passes a directory string and expects MapFS to be substituted
  • Confuses os.DirFS with a way to turn an fs.FS into a path