1.7.3 综合练习:命令行工具开发

1.7.3 综合练习:命令行工具开发 #

在本节中,我们将通过开发一个完整的命令行工具来综合应用前面学到的测试知识。我们将构建一个文件管理工具,并为其编写全面的测试套件,包括单元测试、集成测试和基准测试。

项目概述 #

我们将开发一个名为 filemanager 的命令行工具,它具有以下功能:

  • 文件和目录的基本操作(复制、移动、删除)
  • 文件内容搜索
  • 文件统计信息
  • 批量文件重命名
  • 文件压缩和解压缩

项目结构 #

首先,让我们设计项目的目录结构:

filemanager/
├── main.go
├── cmd/
│   ├── copy.go
│   ├── move.go
│   ├── delete.go
│   ├── search.go
│   ├── stats.go
│   └── rename.go
├── internal/
│   ├── fileops/
│   │   ├── fileops.go
│   │   └── fileops_test.go
│   ├── search/
│   │   ├── search.go
│   │   └── search_test.go
│   └── utils/
│       ├── utils.go
│       └── utils_test.go
├── testdata/
│   ├── sample.txt
│   ├── test_dir/
│   └── archive.zip
└── go.mod

核心功能实现 #

1. 文件操作模块 #

// internal/fileops/fileops.go
package fileops

import (
    "fmt"
    "io"
    "os"
    "path/filepath"
    "time"
)

// FileInfo 表示文件信息
type FileInfo struct {
    Name    string
    Size    int64
    Mode    os.FileMode
    ModTime time.Time
    IsDir   bool
}

// FileManager 文件管理器
type FileManager struct {
    baseDir string
}

// NewFileManager 创建新的文件管理器
func NewFileManager(baseDir string) *FileManager {
    return &FileManager{baseDir: baseDir}
}

// Copy 复制文件或目录
func (fm *FileManager) Copy(src, dst string) error {
    srcPath := filepath.Join(fm.baseDir, src)
    dstPath := filepath.Join(fm.baseDir, dst)

    srcInfo, err := os.Stat(srcPath)
    if err != nil {
        return fmt.Errorf("source file error: %w", err)
    }

    if srcInfo.IsDir() {
        return fm.copyDir(srcPath, dstPath)
    }
    return fm.copyFile(srcPath, dstPath)
}

// copyFile 复制单个文件
func (fm *FileManager) copyFile(src, dst string) error {
    srcFile, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("cannot open source file: %w", err)
    }
    defer srcFile.Close()

    // 确保目标目录存在
    if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
        return fmt.Errorf("cannot create destination directory: %w", err)
    }

    dstFile, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("cannot create destination file: %w", err)
    }
    defer dstFile.Close()

    _, err = io.Copy(dstFile, srcFile)
    if err != nil {
        return fmt.Errorf("copy operation failed: %w", err)
    }

    // 复制文件权限
    srcInfo, err := os.Stat(src)
    if err != nil {
        return fmt.Errorf("cannot get source file info: %w", err)
    }

    return os.Chmod(dst, srcInfo.Mode())
}

// copyDir 递归复制目录
func (fm *FileManager) copyDir(src, dst string) error {
    srcInfo, err := os.Stat(src)
    if err != nil {
        return err
    }

    if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
        return err
    }

    entries, err := os.ReadDir(src)
    if err != nil {
        return err
    }

    for _, entry := range entries {
        srcPath := filepath.Join(src, entry.Name())
        dstPath := filepath.Join(dst, entry.Name())

        if entry.IsDir() {
            if err := fm.copyDir(srcPath, dstPath); err != nil {
                return err
            }
        } else {
            if err := fm.copyFile(srcPath, dstPath); err != nil {
                return err
            }
        }
    }

    return nil
}

// Move 移动文件或目录
func (fm *FileManager) Move(src, dst string) error {
    srcPath := filepath.Join(fm.baseDir, src)
    dstPath := filepath.Join(fm.baseDir, dst)

    // 确保目标目录存在
    if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
        return fmt.Errorf("cannot create destination directory: %w", err)
    }

    return os.Rename(srcPath, dstPath)
}

// Delete 删除文件或目录
func (fm *FileManager) Delete(path string) error {
    fullPath := filepath.Join(fm.baseDir, path)
    return os.RemoveAll(fullPath)
}

// GetFileInfo 获取文件信息
func (fm *FileManager) GetFileInfo(path string) (*FileInfo, error) {
    fullPath := filepath.Join(fm.baseDir, path)
    info, err := os.Stat(fullPath)
    if err != nil {
        return nil, err
    }

    return &FileInfo{
        Name:    info.Name(),
        Size:    info.Size(),
        Mode:    info.Mode(),
        ModTime: info.ModTime(),
        IsDir:   info.IsDir(),
    }, nil
}

// ListFiles 列出目录中的文件
func (fm *FileManager) ListFiles(dir string) ([]*FileInfo, error) {
    fullPath := filepath.Join(fm.baseDir, dir)
    entries, err := os.ReadDir(fullPath)
    if err != nil {
        return nil, err
    }

    var files []*FileInfo
    for _, entry := range entries {
        info, err := entry.Info()
        if err != nil {
            continue
        }

        files = append(files, &FileInfo{
            Name:    info.Name(),
            Size:    info.Size(),
            Mode:    info.Mode(),
            ModTime: info.ModTime(),
            IsDir:   info.IsDir(),
        })
    }

    return files, nil
}

2. 文件操作测试 #

// internal/fileops/fileops_test.go
package fileops

import (
    "os"
    "path/filepath"
    "testing"
    "time"
)

// 测试辅助函数
func setupTestDir(t *testing.T) (string, func()) {
    t.Helper()

    tempDir, err := os.MkdirTemp("", "filemanager_test")
    if err != nil {
        t.Fatalf("Failed to create temp dir: %v", err)
    }

    // 创建测试文件和目录
    testFile := filepath.Join(tempDir, "test.txt")
    if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil {
        t.Fatalf("Failed to create test file: %v", err)
    }

    testDir := filepath.Join(tempDir, "testdir")
    if err := os.Mkdir(testDir, 0755); err != nil {
        t.Fatalf("Failed to create test dir: %v", err)
    }

    nestedFile := filepath.Join(testDir, "nested.txt")
    if err := os.WriteFile(nestedFile, []byte("nested content"), 0644); err != nil {
        t.Fatalf("Failed to create nested file: %v", err)
    }

    cleanup := func() {
        os.RemoveAll(tempDir)
    }

    return tempDir, cleanup
}

func TestFileManager_Copy(t *testing.T) {
    tempDir, cleanup := setupTestDir(t)
    defer cleanup()

    fm := NewFileManager(tempDir)

    t.Run("CopyFile", func(t *testing.T) {
        err := fm.Copy("test.txt", "test_copy.txt")
        if err != nil {
            t.Errorf("Copy file failed: %v", err)
        }

        // 验证文件是否存在
        dstPath := filepath.Join(tempDir, "test_copy.txt")
        if _, err := os.Stat(dstPath); os.IsNotExist(err) {
            t.Error("Copied file does not exist")
        }

        // 验证文件内容
        content, err := os.ReadFile(dstPath)
        if err != nil {
            t.Errorf("Failed to read copied file: %v", err)
        }

        expected := "test content"
        if string(content) != expected {
            t.Errorf("Copied file content = %s; want %s", string(content), expected)
        }
    })

    t.Run("CopyDirectory", func(t *testing.T) {
        err := fm.Copy("testdir", "testdir_copy")
        if err != nil {
            t.Errorf("Copy directory failed: %v", err)
        }

        // 验证目录是否存在
        dstDir := filepath.Join(tempDir, "testdir_copy")
        if _, err := os.Stat(dstDir); os.IsNotExist(err) {
            t.Error("Copied directory does not exist")
        }

        // 验证嵌套文件是否存在
        nestedFile := filepath.Join(dstDir, "nested.txt")
        if _, err := os.Stat(nestedFile); os.IsNotExist(err) {
            t.Error("Nested file in copied directory does not exist")
        }
    })

    t.Run("CopyNonExistentFile", func(t *testing.T) {
        err := fm.Copy("nonexistent.txt", "copy.txt")
        if err == nil {
            t.Error("Expected error when copying non-existent file")
        }
    })
}

func TestFileManager_Move(t *testing.T) {
    tempDir, cleanup := setupTestDir(t)
    defer cleanup()

    fm := NewFileManager(tempDir)

    t.Run("MoveFile", func(t *testing.T) {
        err := fm.Move("test.txt", "moved.txt")
        if err != nil {
            t.Errorf("Move file failed: %v", err)
        }

        // 验证原文件不存在
        srcPath := filepath.Join(tempDir, "test.txt")
        if _, err := os.Stat(srcPath); !os.IsNotExist(err) {
            t.Error("Source file still exists after move")
        }

        // 验证目标文件存在
        dstPath := filepath.Join(tempDir, "moved.txt")
        if _, err := os.Stat(dstPath); os.IsNotExist(err) {
            t.Error("Moved file does not exist")
        }
    })
}

func TestFileManager_Delete(t *testing.T) {
    tempDir, cleanup := setupTestDir(t)
    defer cleanup()

    fm := NewFileManager(tempDir)

    t.Run("DeleteFile", func(t *testing.T) {
        err := fm.Delete("test.txt")
        if err != nil {
            t.Errorf("Delete file failed: %v", err)
        }

        // 验证文件不存在
        filePath := filepath.Join(tempDir, "test.txt")
        if _, err := os.Stat(filePath); !os.IsNotExist(err) {
            t.Error("File still exists after deletion")
        }
    })

    t.Run("DeleteDirectory", func(t *testing.T) {
        err := fm.Delete("testdir")
        if err != nil {
            t.Errorf("Delete directory failed: %v", err)
        }

        // 验证目录不存在
        dirPath := filepath.Join(tempDir, "testdir")
        if _, err := os.Stat(dirPath); !os.IsNotExist(err) {
            t.Error("Directory still exists after deletion")
        }
    })
}

func TestFileManager_GetFileInfo(t *testing.T) {
    tempDir, cleanup := setupTestDir(t)
    defer cleanup()

    fm := NewFileManager(tempDir)

    t.Run("GetFileInfo", func(t *testing.T) {
        info, err := fm.GetFileInfo("test.txt")
        if err != nil {
            t.Errorf("GetFileInfo failed: %v", err)
        }

        if info.Name != "test.txt" {
            t.Errorf("File name = %s; want test.txt", info.Name)
        }

        if info.Size != 12 { // "test content" is 12 bytes
            t.Errorf("File size = %d; want 12", info.Size)
        }

        if info.IsDir {
            t.Error("File should not be directory")
        }
    })

    t.Run("GetDirectoryInfo", func(t *testing.T) {
        info, err := fm.GetFileInfo("testdir")
        if err != nil {
            t.Errorf("GetFileInfo for directory failed: %v", err)
        }

        if !info.IsDir {
            t.Error("Directory should be marked as directory")
        }
    })
}

func TestFileManager_ListFiles(t *testing.T) {
    tempDir, cleanup := setupTestDir(t)
    defer cleanup()

    fm := NewFileManager(tempDir)

    files, err := fm.ListFiles(".")
    if err != nil {
        t.Errorf("ListFiles failed: %v", err)
    }

    if len(files) < 2 {
        t.Errorf("Expected at least 2 files, got %d", len(files))
    }

    // 验证文件名
    fileNames := make(map[string]bool)
    for _, file := range files {
        fileNames[file.Name] = true
    }

    if !fileNames["test.txt"] {
        t.Error("test.txt not found in file list")
    }

    if !fileNames["testdir"] {
        t.Error("testdir not found in file list")
    }
}

// 基准测试
func BenchmarkFileManager_Copy(b *testing.B) {
    tempDir, err := os.MkdirTemp("", "benchmark_test")
    if err != nil {
        b.Fatalf("Failed to create temp dir: %v", err)
    }
    defer os.RemoveAll(tempDir)

    // 创建测试文件
    testFile := filepath.Join(tempDir, "test.txt")
    content := make([]byte, 1024*1024) // 1MB file
    for i := range content {
        content[i] = byte(i % 256)
    }

    if err := os.WriteFile(testFile, content, 0644); err != nil {
        b.Fatalf("Failed to create test file: %v", err)
    }

    fm := NewFileManager(tempDir)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        dstFile := filepath.Join("copy", fmt.Sprintf("test_%d.txt", i))
        if err := fm.Copy("test.txt", dstFile); err != nil {
            b.Errorf("Copy failed: %v", err)
        }
    }
}

func BenchmarkFileManager_ListFiles(b *testing.B) {
    tempDir, err := os.MkdirTemp("", "benchmark_test")
    if err != nil {
        b.Fatalf("Failed to create temp dir: %v", err)
    }
    defer os.RemoveAll(tempDir)

    // 创建多个测试文件
    for i := 0; i < 1000; i++ {
        filename := filepath.Join(tempDir, fmt.Sprintf("file_%d.txt", i))
        if err := os.WriteFile(filename, []byte("content"), 0644); err != nil {
            b.Fatalf("Failed to create test file: %v", err)
        }
    }

    fm := NewFileManager(tempDir)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := fm.ListFiles(".")
        if err != nil {
            b.Errorf("ListFiles failed: %v", err)
        }
    }
}

3. 搜索功能模块 #

// internal/search/search.go
package search

import (
    "bufio"
    "fmt"
    "os"
    "path/filepath"
    "regexp"
    "strings"
)

// SearchResult 搜索结果
type SearchResult struct {
    FilePath   string
    LineNumber int
    Line       string
    Match      string
}

// Searcher 文件搜索器
type Searcher struct {
    baseDir string
}

// NewSearcher 创建新的搜索器
func NewSearcher(baseDir string) *Searcher {
    return &Searcher{baseDir: baseDir}
}

// SearchText 在文件中搜索文本
func (s *Searcher) SearchText(pattern, filePattern string, caseSensitive bool) ([]*SearchResult, error) {
    var results []*SearchResult

    // 编译正则表达式
    flags := 0
    if !caseSensitive {
        flags = regexp.IgnoreCase
    }

    regex, err := regexp.Compile(fmt.Sprintf("(?%s)%s", flagsToString(flags), pattern))
    if err != nil {
        return nil, fmt.Errorf("invalid pattern: %w", err)
    }

    // 遍历文件
    err = filepath.Walk(s.baseDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if info.IsDir() {
            return nil
        }

        // 检查文件名是否匹配
        if filePattern != "" {
            matched, err := filepath.Match(filePattern, info.Name())
            if err != nil || !matched {
                return nil
            }
        }

        // 搜索文件内容
        fileResults, err := s.searchInFile(path, regex)
        if err != nil {
            return nil // 跳过无法读取的文件
        }

        results = append(results, fileResults...)
        return nil
    })

    return results, err
}

// searchInFile 在单个文件中搜索
func (s *Searcher) searchInFile(filePath string, regex *regexp.Regexp) ([]*SearchResult, error) {
    file, err := os.Open(filePath)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    var results []*SearchResult
    scanner := bufio.NewScanner(file)
    lineNumber := 1

    for scanner.Scan() {
        line := scanner.Text()
        if matches := regex.FindAllString(line, -1); len(matches) > 0 {
            for _, match := range matches {
                results = append(results, &SearchResult{
                    FilePath:   filePath,
                    LineNumber: lineNumber,
                    Line:       line,
                    Match:      match,
                })
            }
        }
        lineNumber++
    }

    return results, scanner.Err()
}

// SearchFiles 按文件名搜索文件
func (s *Searcher) SearchFiles(pattern string) ([]string, error) {
    var files []string

    err := filepath.Walk(s.baseDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if info.IsDir() {
            return nil
        }

        matched, err := filepath.Match(pattern, info.Name())
        if err != nil {
            return err
        }

        if matched {
            relPath, err := filepath.Rel(s.baseDir, path)
            if err != nil {
                relPath = path
            }
            files = append(files, relPath)
        }

        return nil
    })

    return files, err
}

// flagsToString 将正则表达式标志转换为字符串
func flagsToString(flags int) string {
    var flagStr strings.Builder
    if flags&regexp.IgnoreCase != 0 {
        flagStr.WriteString("i")
    }
    return flagStr.String()
}

4. 搜索功能测试 #

// internal/search/search_test.go
package search

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

func setupSearchTestDir(t *testing.T) (string, func()) {
    t.Helper()

    tempDir, err := os.MkdirTemp("", "search_test")
    if err != nil {
        t.Fatalf("Failed to create temp dir: %v", err)
    }

    // 创建测试文件
    files := map[string]string{
        "file1.txt": "Hello World\nThis is a test\nHello again",
        "file2.txt": "Another file\nwith different content\nHELLO there",
        "test.log":  "Error: something went wrong\nInfo: operation completed\nError: another issue",
        "data.json": `{"name": "test", "value": 123}`,
    }

    for filename, content := range files {
        filePath := filepath.Join(tempDir, filename)
        if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
            t.Fatalf("Failed to create test file %s: %v", filename, err)
        }
    }

    // 创建子目录
    subDir := filepath.Join(tempDir, "subdir")
    if err := os.Mkdir(subDir, 0755); err != nil {
        t.Fatalf("Failed to create subdirectory: %v", err)
    }

    subFile := filepath.Join(subDir, "nested.txt")
    if err := os.WriteFile(subFile, []byte("nested content\nHello from subdir"), 0644); err != nil {
        t.Fatalf("Failed to create nested file: %v", err)
    }

    cleanup := func() {
        os.RemoveAll(tempDir)
    }

    return tempDir, cleanup
}

func TestSearcher_SearchText(t *testing.T) {
    tempDir, cleanup := setupSearchTestDir(t)
    defer cleanup()

    searcher := NewSearcher(tempDir)

    t.Run("CaseSensitiveSearch", func(t *testing.T) {
        results, err := searcher.SearchText("Hello", "", true)
        if err != nil {
            t.Errorf("SearchText failed: %v", err)
        }

        // 应该找到 3 个匹配项(file1.txt 中的 2 个,nested.txt 中的 1 个)
        if len(results) != 3 {
            t.Errorf("Expected 3 results, got %d", len(results))
        }

        // 验证结果内容
        for _, result := range results {
            if result.Match != "Hello" {
                t.Errorf("Expected match 'Hello', got '%s'", result.Match)
            }
        }
    })

    t.Run("CaseInsensitiveSearch", func(t *testing.T) {
        results, err := searcher.SearchText("hello", "", false)
        if err != nil {
            t.Errorf("SearchText failed: %v", err)
        }

        // 应该找到 4 个匹配项(包括 "HELLO")
        if len(results) != 4 {
            t.Errorf("Expected 4 results, got %d", len(results))
        }
    })

    t.Run("SearchWithFilePattern", func(t *testing.T) {
        results, err := searcher.SearchText("Error", "*.log", true)
        if err != nil {
            t.Errorf("SearchText with file pattern failed: %v", err)
        }

        // 应该只在 .log 文件中找到匹配项
        if len(results) != 2 {
            t.Errorf("Expected 2 results, got %d", len(results))
        }

        for _, result := range results {
            if !filepath.Match("*.log", filepath.Base(result.FilePath)) {
                t.Errorf("Result should be from .log file, got %s", result.FilePath)
            }
        }
    })

    t.Run("RegexSearch", func(t *testing.T) {
        results, err := searcher.SearchText(`\d+`, "", true)
        if err != nil {
            t.Errorf("Regex search failed: %v", err)
        }

        // 应该在 data.json 中找到数字
        if len(results) == 0 {
            t.Error("Expected to find numeric matches")
        }
    })

    t.Run("NoMatches", func(t *testing.T) {
        results, err := searcher.SearchText("nonexistent", "", true)
        if err != nil {
            t.Errorf("SearchText failed: %v", err)
        }

        if len(results) != 0 {
            t.Errorf("Expected 0 results, got %d", len(results))
        }
    })
}

func TestSearcher_SearchFiles(t *testing.T) {
    tempDir, cleanup := setupSearchTestDir(t)
    defer cleanup()

    searcher := NewSearcher(tempDir)

    t.Run("SearchTxtFiles", func(t *testing.T) {
        files, err := searcher.SearchFiles("*.txt")
        if err != nil {
            t.Errorf("SearchFiles failed: %v", err)
        }

        // 应该找到 3 个 .txt 文件
        if len(files) != 3 {
            t.Errorf("Expected 3 .txt files, got %d", len(files))
        }
    })

    t.Run("SearchSpecificFile", func(t *testing.T) {
        files, err := searcher.SearchFiles("data.json")
        if err != nil {
            t.Errorf("SearchFiles failed: %v", err)
        }

        if len(files) != 1 {
            t.Errorf("Expected 1 file, got %d", len(files))
        }

        if files[0] != "data.json" {
            t.Errorf("Expected data.json, got %s", files[0])
        }
    })

    t.Run("SearchNonExistentPattern", func(t *testing.T) {
        files, err := searcher.SearchFiles("*.xyz")
        if err != nil {
            t.Errorf("SearchFiles failed: %v", err)
        }

        if len(files) != 0 {
            t.Errorf("Expected 0 files, got %d", len(files))
        }
    })
}

// 基准测试
func BenchmarkSearcher_SearchText(b *testing.B) {
    tempDir, err := os.MkdirTemp("", "search_benchmark")
    if err != nil {
        b.Fatalf("Failed to create temp dir: %v", err)
    }
    defer os.RemoveAll(tempDir)

    // 创建大量测试文件
    for i := 0; i < 100; i++ {
        filename := filepath.Join(tempDir, fmt.Sprintf("file_%d.txt", i))
        content := strings.Repeat("Hello World\nThis is line number\nAnother line\n", 100)
        if err := os.WriteFile(filename, []byte(content), 0644); err != nil {
            b.Fatalf("Failed to create test file: %v", err)
        }
    }

    searcher := NewSearcher(tempDir)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := searcher.SearchText("Hello", "*.txt", true)
        if err != nil {
            b.Errorf("SearchText failed: %v", err)
        }
    }
}

func BenchmarkSearcher_SearchFiles(b *testing.B) {
    tempDir, err := os.MkdirTemp("", "search_benchmark")
    if err != nil {
        b.Fatalf("Failed to create temp dir: %v", err)
    }
    defer os.RemoveAll(tempDir)

    // 创建大量测试文件
    for i := 0; i < 1000; i++ {
        filename := filepath.Join(tempDir, fmt.Sprintf("file_%d.txt", i))
        if err := os.WriteFile(filename, []byte("content"), 0644); err != nil {
            b.Fatalf("Failed to create test file: %v", err)
        }
    }

    searcher := NewSearcher(tempDir)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := searcher.SearchFiles("*.txt")
        if err != nil {
            b.Errorf("SearchFiles failed: %v", err)
        }
    }
}

5. 主程序和命令行接口 #

// main.go
package main

import (
    "flag"
    "fmt"
    "os"
    "path/filepath"

    "filemanager/internal/fileops"
    "filemanager/internal/search"
)

func main() {
    if len(os.Args) < 2 {
        printUsage()
        os.Exit(1)
    }

    command := os.Args[1]

    switch command {
    case "copy":
        handleCopy()
    case "move":
        handleMove()
    case "delete":
        handleDelete()
    case "search":
        handleSearch()
    case "list":
        handleList()
    default:
        fmt.Printf("Unknown command: %s\n", command)
        printUsage()
        os.Exit(1)
    }
}

func printUsage() {
    fmt.Println("File Manager - A command line file management tool")
    fmt.Println()
    fmt.Println("Usage:")
    fmt.Println("  filemanager copy <source> <destination>")
    fmt.Println("  filemanager move <source> <destination>")
    fmt.Println("  filemanager delete <path>")
    fmt.Println("  filemanager search <pattern> [options]")
    fmt.Println("  filemanager list <directory>")
    fmt.Println()
    fmt.Println("Examples:")
    fmt.Println("  filemanager copy file.txt backup/file.txt")
    fmt.Println("  filemanager search \"Hello\" -file=\"*.txt\" -case=false")
    fmt.Println("  filemanager list .")
}

func handleCopy() {
    if len(os.Args) < 4 {
        fmt.Println("Usage: filemanager copy <source> <destination>")
        os.Exit(1)
    }

    src := os.Args[2]
    dst := os.Args[3]

    fm := fileops.NewFileManager(".")
    if err := fm.Copy(src, dst); err != nil {
        fmt.Printf("Copy failed: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("Successfully copied %s to %s\n", src, dst)
}

func handleMove() {
    if len(os.Args) < 4 {
        fmt.Println("Usage: filemanager move <source> <destination>")
        os.Exit(1)
    }

    src := os.Args[2]
    dst := os.Args[3]

    fm := fileops.NewFileManager(".")
    if err := fm.Move(src, dst); err != nil {
        fmt.Printf("Move failed: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("Successfully moved %s to %s\n", src, dst)
}

func handleDelete() {
    if len(os.Args) < 3 {
        fmt.Println("Usage: filemanager delete <path>")
        os.Exit(1)
    }

    path := os.Args[2]

    fm := fileops.NewFileManager(".")
    if err := fm.Delete(path); err != nil {
        fmt.Printf("Delete failed: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("Successfully deleted %s\n", path)
}

func handleSearch() {
    if len(os.Args) < 3 {
        fmt.Println("Usage: filemanager search <pattern> [options]")
        fmt.Println("Options:")
        fmt.Println("  -file=<pattern>  File pattern to search in")
        fmt.Println("  -case=<bool>     Case sensitive search (default: true)")
        os.Exit(1)
    }

    pattern := os.Args[2]

    // 解析选项
    fs := flag.NewFlagSet("search", flag.ExitOnError)
    filePattern := fs.String("file", "", "File pattern to search in")
    caseSensitive := fs.Bool("case", true, "Case sensitive search")

    fs.Parse(os.Args[3:])

    searcher := search.NewSearcher(".")
    results, err := searcher.SearchText(pattern, *filePattern, *caseSensitive)
    if err != nil {
        fmt.Printf("Search failed: %v\n", err)
        os.Exit(1)
    }

    if len(results) == 0 {
        fmt.Println("No matches found")
        return
    }

    fmt.Printf("Found %d matches:\n", len(results))
    for _, result := range results {
        fmt.Printf("%s:%d: %s\n", result.FilePath, result.LineNumber, result.Line)
    }
}

func handleList() {
    dir := "."
    if len(os.Args) >= 3 {
        dir = os.Args[2]
    }

    fm := fileops.NewFileManager(".")
    files, err := fm.ListFiles(dir)
    if err != nil {
        fmt.Printf("List failed: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("Contents of %s:\n", dir)
    for _, file := range files {
        fileType := "file"
        if file.IsDir {
            fileType = "dir"
        }
        fmt.Printf("%-10s %10d %s %s\n",
            fileType, file.Size, file.ModTime.Format("2006-01-02 15:04"), file.Name)
    }
}

6. 集成测试 #

// integration_test.go
package main

import (
    "os"
    "os/exec"
    "path/filepath"
    "strings"
    "testing"
)

func TestFileManagerIntegration(t *testing.T) {
    // 构建程序
    buildCmd := exec.Command("go", "build", "-o", "filemanager_test")
    if err := buildCmd.Run(); err != nil {
        t.Fatalf("Failed to build program: %v", err)
    }
    defer os.Remove("filemanager_test")

    // 创建测试目录
    tempDir, err := os.MkdirTemp("", "integration_test")
    if err != nil {
        t.Fatalf("Failed to create temp dir: %v", err)
    }
    defer os.RemoveAll(tempDir)

    // 切换到测试目录
    oldDir, err := os.Getwd()
    if err != nil {
        t.Fatalf("Failed to get current dir: %v", err)
    }
    defer os.Chdir(oldDir)

    if err := os.Chdir(tempDir); err != nil {
        t.Fatalf("Failed to change dir: %v", err)
    }

    // 复制程序到测试目录
    programPath := filepath.Join(oldDir, "filemanager_test")
    testProgramPath := filepath.Join(tempDir, "filemanager")
    copyCmd := exec.Command("cp", programPath, testProgramPath)
    if err := copyCmd.Run(); err != nil {
        t.Fatalf("Failed to copy program: %v", err)
    }

    t.Run("CopyCommand", func(t *testing.T) {
        // 创建源文件
        srcFile := "source.txt"
        if err := os.WriteFile(srcFile, []byte("test content"), 0644); err != nil {
            t.Fatalf("Failed to create source file: %v", err)
        }

        // 执行复制命令
        cmd := exec.Command("./filemanager", "copy", srcFile, "destination.txt")
        output, err := cmd.CombinedOutput()
        if err != nil {
            t.Errorf("Copy command failed: %v\nOutput: %s", err, output)
        }

        // 验证目标文件存在
        if _, err := os.Stat("destination.txt"); os.IsNotExist(err) {
            t.Error("Destination file was not created")
        }

        // 验证文件内容
        content, err := os.ReadFile("destination.txt")
        if err != nil {
            t.Errorf("Failed to read destination file: %v", err)
        }

        if string(content) != "test content" {
            t.Errorf("File content mismatch: got %s, want 'test content'", string(content))
        }
    })

    t.Run("SearchCommand", func(t *testing.T) {
        // 创建测试文件
        testFiles := map[string]string{
            "file1.txt": "Hello World\nThis is a test",
            "file2.txt": "Another file\nHello there",
        }

        for filename, content := range testFiles {
            if err := os.WriteFile(filename, []byte(content), 0644); err != nil {
                t.Fatalf("Failed to create test file %s: %v", filename, err)
            }
        }

        // 执行搜索命令
        cmd := exec.Command("./filemanager", "search", "Hello")
        output, err := cmd.CombinedOutput()
        if err != nil {
            t.Errorf("Search command failed: %v\nOutput: %s", err, output)
        }

        // 验证输出包含预期结果
        outputStr := string(output)
        if !strings.Contains(outputStr, "file1.txt") || !strings.Contains(outputStr, "file2.txt") {
            t.Errorf("Search output doesn't contain expected files: %s", outputStr)
        }
    })

    t.Run("ListCommand", func(t *testing.T) {
        // 执行列表命令
        cmd := exec.Command("./filemanager", "list", ".")
        output, err := cmd.CombinedOutput()
        if err != nil {
            t.Errorf("List command failed: %v\nOutput: %s", err, output)
        }

        // 验证输出包含文件列表
        outputStr := string(output)
        if !strings.Contains(outputStr, "Contents of") {
            t.Errorf("List output doesn't contain expected header: %s", outputStr)
        }
    })
}

运行完整测试套件 #

创建一个 Makefile 来管理测试:

# Makefile
.PHONY: test test-unit test-integration test-bench test-cover clean

# 运行所有测试
test: test-unit test-integration

# 运行单元测试
test-unit:
	go test -v ./internal/...

# 运行集成测试
test-integration:
	go test -v -tags=integration .

# 运行基准测试
test-bench:
	go test -bench=. -benchmem ./internal/...

# 运行覆盖率测试
test-cover:
	go test -cover -coverprofile=coverage.out ./internal/...
	go tool cover -html=coverage.out -o coverage.html
	@echo "Coverage report generated: coverage.html"

# 运行所有测试并生成报告
test-all: test-cover test-bench
	@echo "All tests completed"

# 清理生成的文件
clean:
	rm -f coverage.out coverage.html filemanager_test

# 构建程序
build:
	go build -o filemanager .

# 安装依赖
deps:
	go mod tidy

测试最佳实践总结 #

通过这个综合项目,我们应用了以下测试最佳实践:

1. 测试组织 #

  • 将测试文件与源文件放在同一包中
  • 使用表格驱动测试处理多种输入情况
  • 使用子测试组织相关测试用例

2. 测试数据管理 #

  • 使用临时目录进行文件操作测试
  • 创建测试辅助函数减少重复代码
  • 在测试结束后清理测试数据

3. 错误处理测试 #

  • 测试正常情况和异常情况
  • 验证错误类型和错误消息
  • 使用 t.Helper() 标记辅助函数

4. 性能测试 #

  • 编写基准测试评估性能
  • 使用 b.ResetTimer() 排除设置时间
  • 测试不同规模的数据

5. 集成测试 #

  • 测试完整的用户工作流
  • 验证命令行接口的正确性
  • 测试程序的实际行为

6. 测试覆盖率 #

  • 追求高覆盖率但不盲目追求 100%
  • 关注关键路径和边界条件
  • 使用覆盖率工具识别未测试的代码

通过这个综合练习,您已经掌握了 Go 语言测试的核心技能,能够为实际项目编写高质量的测试代码。测试不仅是保证代码质量的手段,更是改善代码设计和提高开发效率的重要工具。