Go测试:如何用虚拟目录结构测试目录复制逻辑?
如何为目录复制函数编写基于桩(Stub)的单元测试
核心思路是将依赖的文件系统操作抽象为接口,这样在测试时可以用模拟实现替换真实的file包调用,完全避免操作真实文件。以下是具体步骤和代码示例(以Go语言为例,思路适配大多数静态类型语言):
1. 抽象文件系统操作接口
先把复制函数用到的file包方法(比如MatchFiles、判断目录、复制文件、创建目录)封装成一个接口,让复制函数依赖这个接口而非直接依赖file包:
// FileSystem 定义文件系统操作的抽象接口 type FileSystem interface { MatchFiles(path string) ([]string, error) // 获取路径下的所有条目 IsDir(path string) bool // 判断是否为目录 CopyFile(src, dst string) error // 复制文件 MkdirAll(path string) error // 递归创建目录 }
2. 实现生产环境用的真实文件系统适配器
把真实的file包方法包装成接口的实现,供正常业务逻辑调用:
// RealFileSystem 对接真实file包的实现 type RealFileSystem struct{} func (r *RealFileSystem) MatchFiles(path string) ([]string, error) { return file.MatchFiles(path) } func (r *RealFileSystem) IsDir(path string) bool { return file.IsDir(path) } func (r *RealFileSystem) CopyFile(src, dst string) error { // 调用真实的文件复制逻辑,比如file.Copy(src, dst) return file.Copy(src, dst) } func (r *RealFileSystem) MkdirAll(path string) error { return file.MkdirAll(path, 0755) }
3. 修改复制函数,依赖抽象接口
调整你的递归复制函数,让它接收FileSystem接口作为参数,不再直接调用file包:
func CopyDir(src, dst string, fs FileSystem) error { entries, err := fs.MatchFiles(src) if err != nil { return err } // 先创建目标目录 if err := fs.MkdirAll(dst); err != nil { return err } for _, entry := range entries { srcPath := filepath.Join(src, entry) dstPath := filepath.Join(dst, entry) if fs.IsDir(srcPath) { // 递归复制子目录 if err := CopyDir(srcPath, dstPath, fs); err != nil { return err } } else { // 复制文件 if err := fs.CopyFile(srcPath, dstPath); err != nil { return err } } } return nil }
4. 编写测试用的模拟(桩)文件系统
创建一个模拟实现,用来预设MatchFiles的返回值、判断目录的结果,同时记录所有文件操作的调用情况,方便后续验证:
// MockFileSystem 测试用的模拟文件系统 type MockFileSystem struct { matchFilesFunc func(path string) ([]string, error) // 自定义MatchFiles的返回 isDirFunc func(path string) bool // 自定义IsDir的返回 copyFileCalls [][2]string // 记录CopyFile的调用参数(src, dst) mkdirAllCalls []string // 记录MkdirAll的调用路径 } func (m *MockFileSystem) MatchFiles(path string) ([]string, error) { if m.matchFilesFunc != nil { return m.matchFilesFunc(path) } return nil, nil } func (m *MockFileSystem) IsDir(path string) bool { if m.isDirFunc != nil { return m.isDirFunc(path) } return false } func (m *MockFileSystem) CopyFile(src, dst string) error { m.copyFileCalls = append(m.copyFileCalls, [2]string{src, dst}) return nil // 测试时无需真实复制,只记录调用 } func (m *MockFileSystem) MkdirAll(path string) error { m.mkdirAllCalls = append(m.mkdirAllCalls, path) return nil // 测试时无需真实创建目录,只记录调用 }
5. 编写单元测试用例
通过配置Mock的行为,模拟不同的目录结构,验证复制逻辑是否正确:
import ( "reflect" "testing" ) func TestCopyDir_RecursiveStructure(t *testing.T) { mockFS := &MockFileSystem{} // 预设MatchFiles的返回:模拟/src下有file1.txt和subdir,/src/subdir下有file2.txt mockFS.matchFilesFunc = func(path string) ([]string, error) { switch path { case "/src": return []string{"file1.txt", "subdir"}, nil case "/src/subdir": return []string{"file2.txt"}, nil default: return nil, nil } } // 预设IsDir的返回:只有/src/subdir是目录 mockFS.isDirFunc = func(path string) bool { return path == "/src/subdir" } // 调用复制函数 err := CopyDir("/src", "/dst", mockFS) if err != nil { t.Fatalf("CopyDir failed unexpectedly: %v", err) } // 验证目录创建是否符合预期 expectedMkdirs := []string{"/dst", "/dst/subdir"} if !reflect.DeepEqual(mockFS.mkdirAllCalls, expectedMkdirs) { t.Errorf("Expected mkdir calls: %v, got: %v", expectedMkdirs, mockFS.mkdirAllCalls) } // 验证文件复制是否符合预期 expectedCopies := [][2]string{ {"/src/file1.txt", "/dst/file1.txt"}, {"/src/subdir/file2.txt", "/dst/subdir/file2.txt"}, } if !reflect.DeepEqual(mockFS.copyFileCalls, expectedCopies) { t.Errorf("Expected copy calls: %v, got: %v", expectedCopies, mockFS.copyFileCalls) } }
额外测试场景建议
- 测试空目录:
MatchFiles返回空数组,验证是否只创建目标目录,无文件复制调用 - 测试错误场景:让
MatchFiles返回错误,验证复制函数是否正确传递错误 - 测试单文件复制:源路径是单个文件,验证是否直接调用
CopyFile
内容的提问来源于stack exchange,提问作者raviabhiram
相关产品推荐
相关产品推荐

