在Golang的单元测试场景中,我们经常需要验证函数返回的错误类型是否符合预期,比如判断返回的是不是参数错误、权限错误等特定类型的错误,而不是仅判断错误内容是否匹配。错误的断言方式会导致测试用例无法准确覆盖异常场景,甚至遗漏潜在的问题。

使用errors.As断言错误类型
Golang 1.13版本之后,标准库errors包提供了As函数,这是官方推荐的断言错误类型的方式,它可以处理错误链中的错误,即使错误被多次包装也能正确匹配目标类型。
首先我们定义一个自定义错误类型,用于测试场景:
package main
import (
"errors"
"fmt"
)
// 定义自定义错误类型
type ParamError struct {
Field string
Msg string
}
// 实现error接口
func (e *ParamError) Error() string {
return fmt.Sprintf("param error: field %s, msg %s", e.Field, e.Msg)
}
// 模拟返回错误的函数
func CheckParam(field string) error {
if field == "" {
return &ParamError{Field: "field", Msg: "cannot be empty"}
}
return nil
}
接下来在测试代码中使用errors.As断言错误类型:
package main
import (
"errors"
"testing"
)
func TestCheckParam(t *testing.T) {
err := CheckParam("")
// 定义目标错误类型的变量
var paramErr *ParamError
// 使用errors.As断言错误类型
if !errors.As(err, ¶mErr) {
t.Errorf("expected ParamError, got %v", err)
}
// 可以进一步校验错误内容
if paramErr.Field != "field" {
t.Errorf("expected field field, got %s", paramErr.Field)
}
}
使用类型断言直接判断
如果确定错误没有被包装,只是原始的错误类型,也可以使用Go原生的类型断言来判断错误类型,这种方式更轻量,但无法处理包装后的错误。
还是使用上面的ParamError类型,测试代码如下:
func TestCheckParamWithTypeAssert(t *testing.T) {
err := CheckParam("")
// 类型断言判断错误类型
paramErr, ok := err.(*ParamError)
if !ok {
t.Errorf("expected ParamError, got %v", err)
}
if paramErr.Msg != "cannot be empty" {
t.Errorf("expected msg cannot be empty, got %s", paramErr.Msg)
}
}
这种方式的局限性在于,如果错误被fmt.Errorf配合%w包装过,类型断言就会失败,比如下面的场景:
func CheckParamWrap(field string) error {
err := CheckParam(field)
if err != nil {
return fmt.Errorf("check param failed: %w", err)
}
return nil
}
此时直接对CheckParamWrap("")的返回结果做类型断言,会得到ok=false,而errors.As依然可以正确匹配到ParamError类型。
断言错误类型时的注意事项
- 优先使用
errors.As进行错误类型断言,尤其是项目中存在错误包装的场景,兼容性更好。 - 自定义错误类型时,建议定义成指针类型实现
Error接口,避免值拷贝带来的问题,同时类型断言时也要对应指针类型。 - 不要仅通过错误字符串判断错误类型,错误内容可能会随着需求变化修改,导致测试用例失效,类型断言的稳定性更高。
- 如果需要判断错误是否是某个特定错误值,可以使用
errors.Is,它和errors.As的区别是前者匹配错误值,后者匹配错误类型。
常见错误场景示例
很多开发者会写出下面这种错误的断言方式:
func TestWrongAssert(t *testing.T) {
err := CheckParam("")
// 错误方式:直接比较错误字符串
if err.Error() != "param error: field field, msg cannot be empty" {
t.Errorf("error not match")
}
}
这种方式的问题在于,一旦ParamError的Error方法实现修改,比如调整了输出格式,测试用例就会失败,而且无法判断错误的具体类型,只能判断内容是否一致。
| 断言方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| errors.As | 需要匹配错误类型,支持错误链 | 兼容性好,支持包装错误 | 需要定义目标类型变量 |
| 类型断言 | 确定错误未被包装,仅需匹配原始类型 | 语法简单,执行效率高 | 无法处理包装后的错误 |
| 错误字符串比较 | 临时快速验证,无类型匹配需求 | 实现简单 | 稳定性差,无法判断类型 |