When does a Go test need t.TempDir instead of an in-memory fstest.MapFS?
answer
- read-only versus writable
- a filesystem value is not a name
- another process needs a real path
- a fresh directory on every call
- removed for you when the test ends
basics
~20 sWhenever the code under test needs a real path: it writes or removes files, creates directories, or hands the name to another process. fstest.MapFS is read-only and is not a path at all, so t.TempDir supplies a real directory instead.
solid answer
~40 sTwo questions decide it. Does the code only read, and does it accept an `fs.FS`? Then `fstest.MapFS` is better — fast, visible in the test source, nothing created and nothing to clean up. Does it write, create directories, remove files, or pass a directory name to something else? Then it needs a real path, because `io/fs` has no write side and a MapFS value is not a name anyone else can open. `t.TempDir()` returns a fresh directory, unique to that test, that the framework removes when the test finishes, so there is no cleanup code and no collision between tests running in parallel. In a mixed component, split it: test the reader against an in-memory tree and the writer under a temporary directory.
code
go · 17 linesdir := t.TempDir()
name := filepath.Join(dir, "2026-03-08.log")
if err := os.WriteFile(name, []byte("ok\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := rotate(dir); err != nil {
t.Fatal(err)
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 {
t.Errorf("got %d files after rotate, want 2", len(entries))
}go deeper
Be able to say that t.TempDir returns a real empty directory the test can write into and that the framework deletes it afterwards, so no cleanup code is needed.
Explain the deciding rule: io/fs is read-only and a filesystem value is not a path, so writing, directory creation and anything handed to another process force a real directory.
Show the split that keeps most tests in memory — an fs.FS reader and a small path-taking writer — and justify preferring the in-memory fixture on speed, hermeticity and readability grounds.
Set the house rule about disk in tests: which suites are allowed to touch the filesystem at all, and whether the read/write split is expected in new components or only when a test forces the question.
## Two fixtures, two different limits `fstest.MapFS` gives you a filesystem **value**: an in-memory `fs.FS` you pass as an argument. `t.TempDir()` gives you a **path**: a string naming a real, empty directory on the machine running the test. Which one a test can use is decided entirely by what the code under test consumes and does. ## Reach for t.TempDir when the code needs a real path Four situations force it. **It writes.** `io/fs` describes a read-only filesystem — there is no create, write, rename or remove in the interface set, so no in-memory `fs.FS` can stand in for a component that produces files. Rotation, compaction, checkpoint writing, "render the report to disk" all need somewhere real. **It creates or removes directories.** Same reason, and the behaviour under test is often exactly the directory manipulation. **It hands the name to something else.** Another process, a library that insists on a directory string, or an operating-system call that takes a path. A value cannot be passed through any of those. **The behaviour is the filesystem's own.** Permission errors, a file that already exists, a name that is a directory when a file was expected. Faking those faithfully is more work than using the real thing. ## What t.TempDir gives you `t.TempDir()` is a method on `*testing.T`. It creates a new directory, returns its path, and registers its removal so the directory disappears when the test — and its subtests — have finished. Each call returns a **different** directory, which is how one test can set up a source tree and a destination tree without inventing names itself: ```go src, dst := t.TempDir(), t.TempDir() ``` Because the directory is per-test and unique, tests that call `t.Parallel()` do not collide, and a failing run leaves nothing behind for the next run to trip over. The alternatives are worse in specific ways. `os.TempDir()` returns the *system* temporary directory, shared by everything on the machine and never cleaned for you. `os.MkdirTemp` creates a unique directory but leaves removal to you, which is a `defer` you will eventually forget in a test that calls `t.Fatal` from a helper. A directory inside the repository survives failures, shows up in `git status`, and is shared by every test that hardcodes it. ## Prefer MapFS where you can When the code only reads and takes an `fs.FS`, the in-memory tree is the better fixture: the input sits three lines above the assertion where a reviewer can see it, there is no I/O, there is nothing to clean up, and metadata such as `ModTime` is a struct field rather than a system call. A test suite that touches disk for fixtures it does not need is slower and produces less readable failures. ## Splitting a mixed component The interesting design pressure appears when one function both reads a directory and writes results into it. As written, the whole thing must be tested against a real directory. Splitting it usually costs one signature change: ```go func plan(fsys fs.FS) ([]string, error) // read side: test with fstest.MapFS func apply(dir string, names []string) error // write side: test with t.TempDir ``` Now the interesting logic — which files are selected, in which order, with which cutoff — is tested in memory against a handful of literals, and only the small mechanical writer needs a real directory. That split is worth doing for its own sake; the testability is a by-product of the read/write boundary becoming explicit. ## A note on assertions With `t.TempDir` the assertion is a real read: open the file you expected the code to produce and compare its contents, or list the directory and compare the set of names. Prefer asserting on what is there over asserting on the exact path string, since the temporary directory's name is generated and must never be hardcoded.
- Two calls to t.TempDir in the same test — one directory or two?Two. Every call creates and returns a new unique directory, which is how a test sets up a source tree and a destination tree without inventing names. All of them are removed when the test and its subtests have finished, so the test writes no cleanup code.
- A function reads a directory and writes its results back into it. How do you test it well?Split it at the read/write boundary: a function taking fs.FS that decides what to do, and a small one taking a directory name that performs the writes. The interesting logic is then tested in memory with fstest.MapFS literals, and only the mechanical writer needs a real directory from t.TempDir.
- Why not create a directory under the repository and delete it at the end of the test?It survives a failed run and pollutes the working tree, every test that hardcodes the name shares it so parallel tests collide, and the deletion is a line you eventually forget. t.TempDir is unique per test and removed by the framework whether the test passes, fails or calls t.Fatal.
- What should the assertion look like after the code has written into the temporary directory?Read the result back: open the expected file and compare its contents, or list the directory and compare the set of names. Never assert on the temporary directory's own path, which is generated and different on every run and every machine.
saying these in an interview costs you the question
- Claims fstest.MapFS supports creating or writing files
- Passes t.TempDir's path where an fs.FS parameter is expected
- Uses os.MkdirTemp and forgets to remove the directory
- Hardcodes a fixed path under /tmp in the test
- Shares one directory across tests that run in parallel
- Asserts on the generated temporary directory's name