Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

testing/fstest 包详解

概述

testing/fstest 包实现了对文件系统实现和使用者的测试支持。

核心功能

  • 文件系统实现测试(TestFS)
  • 内存文件系统(MapFS)
  • 文件操作模拟
  • 符号链接支持
  • 目录遍历测试

重要说明

  • Go 版本:Go 1.16+(io/fs 引入后)
  • ⚠️ 并发限制:MapFS 操作期间不能修改 map(会有 race)
  • ⚠️ 性能考虑:打开或读取目录需要遍历整个 map,建议不超过几百个条目
  • 测试场景:用于测试文件操作而无需真实文件系统

包导入

import "testing/fstest"

类型详解(按 A-Z 分类)

M

MapFS

type MapFS map[string]*MapFile

功能: 简单的内存文件系统,用于测试。

特点

  • 表示为路径名到文件信息的 map
  • 不需要包含父目录,会自动合成
  • 文件操作直接读取 map
  • 操作期间不能并发修改 map(race)
  • 打开/读取目录需要遍历整个 map

实现接口

  • fs.FS
  • fs.ReadDirFS
  • fs.ReadFileFS
  • fs.ReadLinkFS(支持符号链接)
  • fs.StatFS
  • fs.GlobFS
  • fs.SubFS

方法

  • Glob(pattern string) - 文件模式匹配
  • Lstat(name string) - 获取文件信息(不跟随符号链接)
  • Open(name string) - 打开文件
  • ReadDir(name string) - 读取目录
  • ReadFile(name string) - 读取文件内容
  • ReadLink(name string) - 读取符号链接目标
  • Stat(name string) - 获取文件信息
  • Sub(dir string) - 创建子文件系统

示例

package main

import (
    "io/fs"
    "testing/fstest"
)

func main() {
    // 创建内存文件系统
    fsys := fstest.MapFS{
        "hello.txt": &fstest.MapFile{
            Data: []byte("Hello, World!"),
            Mode: 0644,
        },
        "config.json": &fstest.MapFile{
            Data: []byte(`{"key": "value"}`),
            Mode: 0644,
        },
        "dir/subdir/file.txt": &fstest.MapFile{
            Data: []byte("Nested file"),
            Mode: 0644,
        },
    }
    
    // 使用文件系统
    file, _ := fsys.Open("hello.txt")
    defer file.Close()
    
    // 读取文件
    data, _ := fs.ReadFile(fsys, "hello.txt")
    println(string(data)) // Hello, World!
}

MapFS.Glob

func (fsys MapFS) Glob(pattern string) ([]string, error)

功能: 返回所有匹配模式的文件名。

参数

  • pattern string - 文件模式(支持 *、? 等)

返回值

  • []string - 匹配的文件名列表
  • error - 错误

示例

fsys := fstest.MapFS{
    "a.txt": &fstest.MapFile{},
    "b.txt": &fstest.MapFile{},
    "c.md":  &fstest.MapFile{},
}

matches, _ := fsys.Glob("*.txt")
fmt.Println(matches) // [a.txt b.txt]

MapFS.Lstat

func (fsys MapFS) Lstat(name string) (fs.FileInfo, error)

功能: 返回文件的 FileInfo。如果是符号链接,返回符号链接本身的信息。

参数

  • name string - 文件路径

返回值

  • fs.FileInfo - 文件信息
  • error - 错误

注意

  • 不跟随符号链接

示例

fsys := fstest.MapFS{
    "link": &fstest.MapFile{
        Data: []byte("target.txt"),
        Mode: fs.ModeSymlink,
    },
}

info, _ := fsys.Lstat("link")
fmt.Println(info.Mode()&fs.ModeSymlink != 0) // true

MapFS.Open

func (fsys MapFS) Open(name string) (fs.File, error)

功能: 打开命名的文件(跟随符号链接)。

参数

  • name string - 文件路径

返回值

  • fs.File - 文件对象
  • error - 错误

示例

fsys := fstest.MapFS{
    "test.txt": &fstest.MapFile{
        Data: []byte("content"),
        Mode: 0644,
    },
}

file, err := fsys.Open("test.txt")
if err != nil {
    // 处理错误
}
defer file.Close()

data := make([]byte, 7)
file.Read(data)
fmt.Println(string(data)) // content

MapFS.ReadDir

func (fsys MapFS) ReadDir(name string) ([]fs.DirEntry, error)

功能: 读取目录内容。

参数

  • name string - 目录路径

返回值

  • []fs.DirEntry - 目录条目列表
  • error - 错误

示例

fsys := fstest.MapFS{
    "dir/a.txt": &fstest.MapFile{},
    "dir/b.txt": &fstest.MapFile{},
    "dir/c.md":  &fstest.MapFile{},
}

entries, _ := fsys.ReadDir("dir")
for _, entry := range entries {
    fmt.Println(entry.Name())
}
// a.txt
// b.txt
// c.md

MapFS.ReadFile

func (fsys MapFS) ReadFile(name string) ([]byte, error)

功能: 读取整个文件内容。

参数

  • name string - 文件路径

返回值

  • []byte - 文件内容
  • error - 错误

示例

fsys := fstest.MapFS{
    "config.yaml": &fstest.MapFile{
        Data: []byte("key: value\n"),
    },
}

data, err := fsys.ReadFile("config.yaml")
if err != nil {
    // 处理错误
}
fmt.Println(string(data)) // key: value
func (fsys MapFS) ReadLink(name string) (string, error)

功能: 返回符号链接的目标。

参数

  • name string - 符号链接路径

返回值

  • string - 链接目标
  • error - 错误

示例

fsys := fstest.MapFS{
    "link": &fstest.MapFile{
        Data: []byte("target.txt"),
        Mode: fs.ModeSymlink,
    },
}

target, _ := fsys.ReadLink("link")
fmt.Println(target) // target.txt

MapFS.Stat

func (fsys MapFS) Stat(name string) (fs.FileInfo, error)

功能: 返回文件的 FileInfo。

参数

  • name string - 文件路径

返回值

  • fs.FileInfo - 文件信息
  • error - 错误

注意

  • 跟随符号链接

示例

fsys := fstest.MapFS{
    "file.txt": &fstest.MapFile{
        Data: []byte("content"),
        Mode: 0644,
    },
}

info, _ := fsys.Stat("file.txt")
fmt.Println(info.Size())  // 7
fmt.Println(info.Mode())  // -rw-r--r--

MapFS.Sub

func (fsys MapFS) Sub(dir string) (fs.FS, error)

功能: 创建以 dir 为根的子文件系统。

参数

  • dir string - 目录路径

返回值

  • fs.FS - 子文件系统
  • error - 错误

示例

fsys := fstest.MapFS{
    "root/a.txt": &fstest.MapFile{},
    "root/b.txt": &fstest.MapFile{},
    "other.txt":  &fstest.MapFile{},
}

sub, _ := fsys.Sub("root")
data, _ := fs.ReadFile(sub, "a.txt")
// other.txt 在子文件系统中不存在

MapFile

type MapFile struct {
    Data    []byte      // 文件内容
    Mode    fs.FileMode // 文件模式
    ModTime time.Time   // 修改时间
    Sys     any         // 额外数据
}

功能: 描述 MapFS 中的单个文件。

字段

  • Data []byte - 文件内容(对于符号链接是目标路径)
  • Mode fs.FileMode - 文件模式和权限
  • ModTime time.Time - 最后修改时间
  • Sys any - 额外系统特定数据

示例

// 普通文件
file := &fstest.MapFile{
    Data:    []byte("Hello"),
    Mode:    0644,
    ModTime: time.Now(),
}

// 目录
dir := &fstest.MapFile{
    Mode: fs.ModeDir | 0755,
}

// 符号链接
link := &fstest.MapFile{
    Data: []byte("target.txt"),
    Mode: fs.ModeSymlink,
}

函数详解(按 A-Z 分类)

T

TestFS

func TestFS(fsys fs.FS, expected ...string) error

功能: 测试文件系统实现。

参数

  • fsys fs.FS - 要测试的文件系统
  • expected ...string - 期望存在的文件列表

返回值

  • error - 第一个错误或错误列表

测试内容

  • 遍历 fsys 中的所有文件
  • 打开并检查每个文件行为是否正确
  • 不跟随符号链接,但检查 Lstat 值
  • 检查文件系统是否包含所有期望的文件
  • 如果没有列出期望文件,fsys 必须为空

注意

  • fsys 的内容不能在 TestFS 执行期间并发修改
  • 发现多个问题时返回第一个错误或错误列表
  • 使用 errors.Is 或 errors.As 检查错误

示例

package mypackage_test

import (
    "testing"
    "testing/fstest"
)

func TestMyFS(t *testing.T) {
    fsys := fstest.MapFS{
        "file1.txt": &fstest.MapFile{
            Data: []byte("content1"),
            Mode: 0644,
        },
        "file2.txt": &fstest.MapFile{
            Data: []byte("content2"),
            Mode: 0644,
        },
        "dir/file3.txt": &fstest.MapFile{
            Data: []byte("content3"),
            Mode: 0644,
        },
    }
    
    // 测试文件系统
    if err := fstest.TestFS(fsys, "file1.txt", "file2.txt", "dir/file3.txt"); err != nil {
        t.Fatal(err)
    }
}

典型用法

func TestCustomFS(t *testing.T) {
    myFS := NewCustomFS()
    
    // 验证文件系统实现
    if err := fstest.TestFS(myFS, "expected/file.txt"); err != nil {
        t.Fatal(err)
    }
}

典型示例

示例 1:基本 MapFS 使用

package main

import (
    "fmt"
    "io/fs"
    "testing/fstest"
)

func main() {
    fsys := fstest.MapFS{
        "hello.txt": &fstest.MapFile{
            Data: []byte("Hello, World!"),
            Mode: 0644,
        },
    }
    
    // 读取文件
    data, err := fs.ReadFile(fsys, "hello.txt")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data)) // Hello, World!
}

示例 2:测试文件系统实现

package myfs_test

import (
    "testing"
    "testing/fstest"
)

func TestMyFileSystem(t *testing.T) {
    fsys := fstest.MapFS{
        "config.yaml": &fstest.MapFile{
            Data: []byte("key: value\n"),
            Mode: 0644,
        },
        "data/file.txt": &fstest.MapFile{
            Data: []byte("data content"),
            Mode: 0644,
        },
    }
    
    // 验证文件系统
    if err := fstest.TestFS(fsys, "config.yaml", "data/file.txt"); err != nil {
        t.Fatal(err)
    }
}

示例 3:带符号链接的文件系统

package main

import (
    "fmt"
    "io/fs"
    "testing/fstest"
)

func main() {
    fsys := fstest.MapFS{
        "target.txt": &fstest.MapFile{
            Data: []byte("target content"),
            Mode: 0644,
        },
        "link.txt": &fstest.MapFile{
            Data: []byte("target.txt"),
            Mode: fs.ModeSymlink,
        },
    }
    
    // ReadFile 会跟随符号链接
    data, _ := fs.ReadFile(fsys, "link.txt")
    fmt.Println(string(data)) // target content
    
    // ReadLink 返回链接目标
    target, _ := fsys.ReadLink("link.txt")
    fmt.Println(target) // target.txt
}

示例 4:目录操作

package main

import (
    "fmt"
    "testing/fstest"
)

func main() {
    fsys := fstest.MapFS{
        "dir/a.txt": &fstest.MapFile{},
        "dir/b.txt": &fstest.MapFile{},
        "dir/sub/c.txt": &fstest.MapFile{},
    }
    
    // 读取目录
    entries, _ := fsys.ReadDir("dir")
    fmt.Println("目录内容:")
    for _, entry := range entries {
        fmt.Println(" -", entry.Name())
    }
    // 输出:
    // 目录内容:
    //  - a.txt
    //  - b.txt
    //  - sub/
}

示例 5:文件模式匹配

package main

import (
    "fmt"
    "testing/fstest"
)

func main() {
    fsys := fstest.MapFS{
        "a.go":   &fstest.MapFile{},
        "b.go":   &fstest.MapFile{},
        "c.txt":  &fstest.MapFile{},
        "dir/d.go": &fstest.MapFile{},
    }
    
    // 匹配 .go 文件
    matches, _ := fsys.Glob("*.go")
    fmt.Println("Go 文件:", matches) // [a.go b.go]
    
    // 匹配所有文件
    matches, _ = fsys.Glob("*/*")
    fmt.Println("子目录文件:", matches) // [dir/d.go]
}

示例 6:测试文件读取函数

package mypackage

import (
    "io/fs"
    "testing"
    "testing/fstest"
)

// 被测试的函数
func LoadConfig(fsys fs.FS) ([]byte, error) {
    return fs.ReadFile(fsys, "config.json")
}

// 测试
func TestLoadConfig(t *testing.T) {
    fsys := fstest.MapFS{
        "config.json": &fstest.MapFile{
            Data: []byte(`{"key": "value"}`),
        },
    }
    
    data, err := LoadConfig(fsys)
    if err != nil {
        t.Fatal(err)
    }
    
    expected := `{"key": "value"}`
    if string(data) != expected {
        t.Errorf("got %q, want %q", data, expected)
    }
}

示例 7:测试目录遍历

package mypackage

import (
    "io/fs"
    "testing"
    "testing/fstest"
)

// 被测试的函数:统计文件数量
func CountFiles(fsys fs.FS) (int, error) {
    count := 0
    err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if !d.IsDir() {
            count++
        }
        return nil
    })
    return count, err
}

// 测试
func TestCountFiles(t *testing.T) {
    fsys := fstest.MapFS{
        "a.txt": &fstest.MapFile{},
        "b.txt": &fstest.MapFile{},
        "dir/c.txt": &fstest.MapFile{},
        "dir/d.txt": &fstest.MapFile{},
    }
    
    count, err := CountFiles(fsys)
    if err != nil {
        t.Fatal(err)
    }
    
    if count != 4 {
        t.Errorf("got %d files, want 4", count)
    }
}

示例 8:测试带权限的文件系统

package mypackage

import (
    "io/fs"
    "testing"
    "testing/fstest"
)

func TestFilePermissions(t *testing.T) {
    fsys := fstest.MapFS{
        "readonly.txt": &fstest.MapFile{
            Data: []byte("content"),
            Mode: 0444, // 只读
        },
        "executable.sh": &fstest.MapFile{
            Data: []byte("#!/bin/sh"),
            Mode: 0755,
        },
    }
    
    // 测试只读文件
    info, _ := fsys.Stat("readonly.txt")
    if info.Mode()&0444 == 0 {
        t.Error("文件应该是只读的")
    }
    
    // 测试可执行文件
    info, _ = fsys.Stat("executable.sh")
    if info.Mode()&0111 == 0 {
        t.Error("文件应该是可执行的")
    }
}

最佳实践

1. 使用 MapFS 进行单元测试

// ✅ 推荐:使用 MapFS
func TestReadFile(t *testing.T) {
    fsys := fstest.MapFS{
        "test.txt": &fstest.MapFile{
            Data: []byte("test"),
        },
    }
    // 测试代码
}

// ❌ 不推荐:使用真实文件系统
func TestReadFile(t *testing.T) {
    os.WriteFile("/tmp/test.txt", []byte("test"), 0644)
    defer os.Remove("/tmp/test.txt")
    // 测试代码
}

2. 使用 TestFS 验证文件系统实现

// ✅ 推荐:使用 TestFS
func TestMyFS(t *testing.T) {
    myFS := NewMyFS()
    if err := fstest.TestFS(myFS, "expected.txt"); err != nil {
        t.Fatal(err)
    }
}

3. 避免并发修改

// ✅ 推荐:顺序操作
fsys := fstest.MapFS{"file.txt": &fstest.MapFile{}}
// 使用 fsys...
// 修改 fsys...

// ❌ 不推荐:并发修改
go func() {
    fsys["new.txt"] = &fstest.MapFile{} // race!
}()

4. 保持 MapFS 简洁

// ✅ 推荐:少量文件
fsys := fstest.MapFS{
    "file1.txt": &fstest.MapFile{},
    "file2.txt": &fstest.MapFile{},
}

// ⚠️ 不推荐:大量文件(性能问题)
fsys := fstest.MapFS{
    // 数千个文件...
}

与其他包配合

与 io/fs 包配合

package main

import (
    "fmt"
    "io/fs"
    "testing/fstest"
)

func main() {
    fsys := fstest.MapFS{
        "hello.txt": &fstest.MapFile{
            Data: []byte("Hello"),
        },
    }
    
    // 使用 io/fs 函数
    data, _ := fs.ReadFile(fsys, "hello.txt")
    fmt.Println(string(data))
    
    // 遍历文件系统
    fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        fmt.Println(path)
        return nil
    })
}

与 path/filepath 配合

package main

import (
    "path/filepath"
    "testing/fstest"
)

func main() {
    fsys := fstest.MapFS{
        filepath.Join("dir", "file.txt"): &fstest.MapFile{
            Data: []byte("content"),
        },
    }
    
    // 使用 fsys...
}

注意事项

限制

  1. 并发限制

    • MapFS 操作期间不能修改 map
    • 会导致 race condition
  2. 性能考虑

    • 打开/读取目录需要遍历整个 map
    • 建议不超过几百个条目
  3. 符号链接

    • 不支持绝对路径符号链接
    • TestFS 不跟随符号链接

使用建议

  1. 测试隔离

    • 每个测试创建独立的 MapFS
    • 避免测试间相互影响
  2. 文件路径

    • 使用正斜杠 / 分隔路径
    • 路径不能以 / 开头或结尾
    • 不能包含 ... 元素
  3. 错误处理

    • TestFS 可能返回多个错误
    • 使用 errors.Is 或 errors.As 检查

快速参考

MapFS 方法速查

方法功能接口
Open打开文件fs.FS
ReadDir读取目录fs.ReadDirFS
ReadFile读取文件fs.ReadFileFS
ReadLink读取符号链接fs.ReadLinkFS
Stat获取文件信息fs.StatFS
Lstat获取文件信息(不跟随链接)fs.ReadLinkFS
Sub创建子文件系统fs.SubFS
Glob文件模式匹配fs.GlobFS

MapFile 字段

字段类型描述
Data[]byte文件内容或符号链接目标
Modefs.FileMode文件模式和权限
ModTimetime.Time最后修改时间
Sysany额外系统数据

常见文件模式

// 普通文件
0644 // -rw-r--r--
0755 // -rwxr-xr-x
0444 // -r--r--r--(只读)

// 目录
fs.ModeDir | 0755

// 符号链接
fs.ModeSymlink

// 组合
fs.ModeDir | fs.ModeSymlink

测试模式

// 基本测试
if err := fstest.TestFS(fsys); err != nil {
    t.Fatal(err)
}

// 带期望文件
if err := fstest.TestFS(fsys, "file1.txt", "file2.txt"); err != nil {
    t.Fatal(err)
}

// 空文件系统测试
if err := fstest.TestFS(fstest.MapFS{}); err != nil {
    t.Fatal(err)
}

总结

testing/fstest 包是 Go 标准库中用于测试文件系统实现的核心包。

核心优势

  • ✅ 内存级文件系统,零 IO 开销
  • ✅ 完全可控,可模拟各种场景
  • ✅ 无缝兼容 io/fs 接口
  • ✅ 支持符号链接
  • ✅ 提供 TestFS 自动测试

重要限制

  • ⚠️ 不能并发修改 MapFS
  • ⚠️ 大量文件时性能下降
  • ⚠️ 不支持绝对路径符号链接

主要用途

  • 文件系统实现测试
  • 文件操作单元测试
  • 模拟文件系统错误场景
  • 测试配置文件处理
  • 测试日志系统

使用建议

  1. 使用 MapFS 替代真实文件系统
  2. 使用 TestFS 验证文件系统实现
  3. 避免并发修改 MapFS
  4. 保持 MapFS 简洁(几百个条目内)
  5. 每个测试创建独立的 MapFS

典型用法

fsys := fstest.MapFS{
    "file.txt": &fstest.MapFile{
        Data: []byte("content"),
        Mode: 0644,
    },
}
if err := fstest.TestFS(fsys, "file.txt"); err != nil {
    t.Fatal(err)
}