155 lines
3.8 KiB
Go
155 lines
3.8 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ------------------------------
|
||
// 错误类型定义(核心优化)
|
||
// ------------------------------
|
||
|
||
// FileError 自定义文件服务错误结构体
|
||
type FileError struct {
|
||
Code int // 错误码
|
||
Module string // 所属模块
|
||
Message string // 错误信息
|
||
Err error // 原始错误(支持错误包装/解包)
|
||
}
|
||
|
||
// Error 实现 error 接口
|
||
func (e *FileError) Error() string {
|
||
if e.Err != nil {
|
||
return fmt.Sprintf("[%s:%d] %s: %v", e.Module, e.Code, e.Message, e.Err)
|
||
}
|
||
return fmt.Sprintf("[%s:%d] %s", e.Module, e.Code, e.Message)
|
||
}
|
||
|
||
// Unwrap 实现 errors.Unwrap 支持错误链解包
|
||
func (e *FileError) Unwrap() error {
|
||
return e.Err
|
||
}
|
||
|
||
// 预定义错误(使用 sentinel error 规范)
|
||
var ErrFileNotFound = errors.New("file not found")
|
||
|
||
// ------------------------------
|
||
// 业务服务实现
|
||
// ------------------------------
|
||
|
||
// FileService 基础文件服务
|
||
func FileService(ctx context.Context, filename string) error {
|
||
// 模块名称常量
|
||
const module = "FileService"
|
||
|
||
// 1. 空文件名校验(400)
|
||
if filename == "" {
|
||
return &FileError{
|
||
Code: 400,
|
||
Module: module,
|
||
Message: "filename cannot be empty",
|
||
}
|
||
}
|
||
|
||
// 2. 文件不存在模拟(404)
|
||
return &FileError{
|
||
Code: 404,
|
||
Module: module,
|
||
Message: "file does not exist",
|
||
Err: ErrFileNotFound, // 包装原始哨兵错误
|
||
}
|
||
}
|
||
|
||
// AdvancedFileService 高级文件服务(包装基础服务)
|
||
func AdvancedFileService(ctx context.Context, filename string) error {
|
||
// 调用底层服务并包装错误信息
|
||
if err := FileService(ctx, filename); err != nil {
|
||
return fmt.Errorf("advanced file service failed: %s %w", filename, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ------------------------------go vet
|
||
// 测试用例
|
||
// ------------------------------
|
||
|
||
func TestFileService(t *testing.T) {
|
||
ctx := context.Background()
|
||
|
||
// 子测试用例(清晰分组,便于定位失败)
|
||
t.Run("file not found returns 404 error", func(t *testing.T) {
|
||
err := FileService(ctx, "test.txt")
|
||
|
||
// 断言哨兵错误
|
||
if !errors.Is(err, ErrFileNotFound) {
|
||
t.Fatalf("期望错误: %v, 实际错误: %v", ErrFileNotFound, err)
|
||
}
|
||
|
||
// 断言自定义错误类型与字段
|
||
var fErr *FileError
|
||
if !errors.As(err, &fErr) {
|
||
t.Fatal("期望返回 *FileError 类型")
|
||
}
|
||
if fErr.Code != 404 {
|
||
t.Errorf("错误码期望 404, 实际 %d", fErr.Code)
|
||
}
|
||
if fErr.Module != "FileService" {
|
||
t.Errorf("模块期望 FileService, 实际 %s", fErr.Module)
|
||
}
|
||
})
|
||
|
||
t.Run("empty filename returns 400 error", func(t *testing.T) {
|
||
err := FileService(ctx, "")
|
||
if err == nil {
|
||
t.Fatal("空文件名必须返回错误")
|
||
}
|
||
|
||
var fErr *FileError
|
||
if !errors.As(err, &fErr) {
|
||
t.Fatal("期望返回 *FileError 类型")
|
||
}
|
||
if fErr.Code != 400 {
|
||
t.Errorf("错误码期望 400, 实际 %d", fErr.Code)
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestErrorWrapping(t *testing.T) {
|
||
original := errors.New("original error")
|
||
wrapped := fmt.Errorf("wrapped: %w", original)
|
||
|
||
// 测试 errors.Is 错误链匹配
|
||
if !errors.Is(wrapped, original) {
|
||
t.Error("errors.Is 应当匹配包装后的错误")
|
||
}
|
||
|
||
// 测试 errors.Unwrap 解包
|
||
unwrapped := errors.Unwrap(wrapped)
|
||
if unwrapped != original {
|
||
t.Errorf("解包后应当返回原始错误, 实际: %v", unwrapped)
|
||
}
|
||
}
|
||
|
||
func TestAdvancedFileService(t *testing.T) {
|
||
ctx := context.Background()
|
||
|
||
err := AdvancedFileService(ctx, "nonexistent.txt")
|
||
if err == nil {
|
||
t.Fatal("必须返回错误")
|
||
}
|
||
|
||
// 错误信息包含文件名
|
||
if !strings.Contains(err.Error(), "nonexistent.txt") {
|
||
t.Errorf("错误信息应当包含文件名, 实际: %v", err)
|
||
}
|
||
|
||
// 错误链中能找到自定义 FileError
|
||
var fErr *FileError
|
||
if !errors.As(err, &fErr) {
|
||
t.Error("错误链中应当包含 *FileError")
|
||
}
|
||
}
|