如何在惯用的 Go 中测试预期的格式错误?
How to test an expected formatted error in idiomatic Go?
这个问题与验证无关。这只是一个例子,但可能是任何其他格式错误的东西。
假设我有一个非常简单的 Validate()
方法来验证扩展,例如。 .vcf
来自文件名,如果它们不匹配,我想要一个错误,指出有效扩展名和当前扩展名。理想情况下,可以扩展此方法以执行其他验证和 return 其他验证错误。
类似于:
// main.go
package main
import (
"fmt"
"os"
"path/filepath"
)
type ValidationError struct {
Msg string
}
func (v ValidationError) Error() string {
return v.Msg
}
func Validate(fileName string) error {
// first validation
wantExtension := ".vcf"
gotExtension := filepath.Ext(fileName)
if gotExtension != wantExtension {
return ValidationError{Msg: fmt.Sprintf("Extension %q not accepted, please use a %s file.", gotExtension, wantExtension)}
}
// potentially other validations
return nil
}
func main() {
fileName := os.Args[1]
Validate(fileName)
}
我想以一种不仅检查错误类型(在本例中是错误,还检查消息)的方式对其进行测试。类似于:
// main_test.go
package main
import (
"errors"
"testing"
)
func TestValidate(t *testing.T) {
testCases := []struct {
name string
fileName string
expectedErr error
}{
{
name: "happy-path",
fileName: "file.vcf",
},
{
name: "wrong-extension",
fileName: "file.md",
expectedErr: ValidationError{Msg: "Extension .md not accepted, please use a .vcf file."},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := Validate(tc.fileName)
if !errors.Is(err, tc.expectedErr) {
t.Errorf("want (%v) got (%v)", tc.expectedErr, err)
}
})
}
}
本次测试将获得:
--- FAIL: TestValidate (0.00s)
--- FAIL: TestValidate/wrong-extension (0.00s)
main_test.go:28: want (Extension .md not accepted, please use a .vcf file.) got (Extension ".md" not accepted, please use a .vcf file.)
我无法工作,因为 errors.Is()
需要相同的内存对象。此外,如果我使用 errors.As()
,测试将通过,但它不会真正检查消息是否正确。
我还尝试定义一个函数来 return 以消息作为参数的错误或更简单的 errors.New()
而不是自定义错误,但我 运行 遇到与对象相同的问题测试端内存不同,也不知道这三种方式中哪一种比较地道。
你会如何实施?
更新:我更新了问题代码以使用自定义错误而不是 errors.New
因为它可能更适合@kingkupps 的回答中的情况。
我建议定义您自己的类型并让该类型实现 Error
接口。在您的测试中,您可以使用 errors.As
来确定返回的错误是新类型的实例还是包装新类型实例的错误。
package main
import (
"errors"
"fmt"
"path/filepath"
)
type ValidationError struct {
WantExtension string
GotExtension string
}
func (v ValidationError) Error() string {
return fmt.Sprintf("extension %q not accepted, please use a %s file.", v.GotExtension, v.WantExtension)
}
func Validate(fileName string) error {
fileExtension := filepath.Ext(fileName)
if fileExtension != ".vcf" {
return ValidationError{WantExtension: ".vcf", GotExtension: fileExtension}
}
return nil
}
func main() {
fileName := "something.jpg"
err := Validate(fileName)
var vErr ValidationError
if errors.As(err, &vErr) {
// you can now use vErr.WantExtension and vErr.GotExtension
fmt.Println(vErr)
fmt.Println(vErr.WantExtension)
fmt.Println(vErr.GotExtension)
}
}
打印:
extension ".jpg" not accepted, please use a .vcf file.
.vcf
.jpg
然后在您的测试中,您可以根据 Error()
的结果或它们的字段比较错误:
func TestValidate(t *testing.T) {
testCases := []struct {
name string
fileName string
expectedErr error
}{
{
name: "happy-path",
fileName: "file.vcf",
},
{
name: "wrong-extension",
fileName: "file.md",
expectedErr: ValidationError{WantExtension: ".vcf", GotExtension: ".md"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := Validate(tc.fileName)
if err != nil {
if tc.expectedErr == nil {
t.Errorf("unexpected error: %v", err)
return
}
var want ValidationError
var got ValidationError
if w, g := errors.As(tc.expectedErr, &want), errors.As(err, &got); w == g && w {
// Both errors are ValidationErrors
assertValidationErrorsEqual(t, want, got)
} else if w != g {
// The expected and actual error differ in type
t.Errorf("wanted error of type %T, got error of type %T", tc.expectedErr, err)
} else {
// Neither error is a ValidationError so we just assert that they produce the same error message
assertErrorMessagesEqual(t, tc.expectedErr, err)
}
}
})
}
}
func assertErrorMessagesEqual(t *testing.T, want error, got error) {
if w, g := want.Error(), got.Error(); w != g {
t.Errorf("want %q, got %q", w, g)
}
}
func assertValidationErrorsEqual(t *testing.T, want ValidationError, got ValidationError) {
if want.WantExtension != got.WantExtension || want.GotExtension != got.GotExtension {
t.Errorf("want %v, got %v", want, got)
}
}
编辑:感谢@Daniel Farrell 关于在类型断言上使用 errors.As
的建议。使用 errors.As
将执行您想要的操作,即使返回的错误包含 ValidationError
的实例,而类型断言不会。
编辑:添加了使用建议方法进行测试的结果。
使用errors.Is
没有问题。代码在使用它时没有失败。
是测试代码没有设置正确
在程序中,错误信息定义为Extension ".md" not accepted, please use a .vcf file.
,如return ValidationError{Msg: fmt.Sprintf("Extension %q not accepted, please use a %s file.", gotExtension, wantExtension)}
在测试中,错误信息定义为expectedErr: ValidationError{Msg: "Extension .md not accepted, please use a .vcf file."},
因此,当尝试比较两个错误值时,状态类似于
fmt.Println(
errors.Is(
ValidationError{Msg: `Extension ".md" not accepted, please use a .vcf file.`},
ValidationError{Msg: `Extension .md not accepted, please use a .vcf file.`},
),
)
使用 api 时预期 return false,因为它执行可比值的相等性检查。
https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/errors/wrap.go;l=46
供参考,大致相当于
fmt.Println(
ValidationError{Msg: `Extension ".md" not accepted, please use a .vcf file.`} ==
ValidationError{Msg: `Extension .md not accepted, please use a .vcf file.`},
)
https://go.dev/ref/spec#Comparison_operators
Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.
肯定是使用错误,这个也应该提到
Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.
这个问题与验证无关。这只是一个例子,但可能是任何其他格式错误的东西。
假设我有一个非常简单的 Validate()
方法来验证扩展,例如。 .vcf
来自文件名,如果它们不匹配,我想要一个错误,指出有效扩展名和当前扩展名。理想情况下,可以扩展此方法以执行其他验证和 return 其他验证错误。
类似于:
// main.go
package main
import (
"fmt"
"os"
"path/filepath"
)
type ValidationError struct {
Msg string
}
func (v ValidationError) Error() string {
return v.Msg
}
func Validate(fileName string) error {
// first validation
wantExtension := ".vcf"
gotExtension := filepath.Ext(fileName)
if gotExtension != wantExtension {
return ValidationError{Msg: fmt.Sprintf("Extension %q not accepted, please use a %s file.", gotExtension, wantExtension)}
}
// potentially other validations
return nil
}
func main() {
fileName := os.Args[1]
Validate(fileName)
}
我想以一种不仅检查错误类型(在本例中是错误,还检查消息)的方式对其进行测试。类似于:
// main_test.go
package main
import (
"errors"
"testing"
)
func TestValidate(t *testing.T) {
testCases := []struct {
name string
fileName string
expectedErr error
}{
{
name: "happy-path",
fileName: "file.vcf",
},
{
name: "wrong-extension",
fileName: "file.md",
expectedErr: ValidationError{Msg: "Extension .md not accepted, please use a .vcf file."},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := Validate(tc.fileName)
if !errors.Is(err, tc.expectedErr) {
t.Errorf("want (%v) got (%v)", tc.expectedErr, err)
}
})
}
}
本次测试将获得:
--- FAIL: TestValidate (0.00s)
--- FAIL: TestValidate/wrong-extension (0.00s)
main_test.go:28: want (Extension .md not accepted, please use a .vcf file.) got (Extension ".md" not accepted, please use a .vcf file.)
我无法工作,因为 errors.Is()
需要相同的内存对象。此外,如果我使用 errors.As()
,测试将通过,但它不会真正检查消息是否正确。
我还尝试定义一个函数来 return 以消息作为参数的错误或更简单的 errors.New()
而不是自定义错误,但我 运行 遇到与对象相同的问题测试端内存不同,也不知道这三种方式中哪一种比较地道。
你会如何实施?
更新:我更新了问题代码以使用自定义错误而不是 errors.New
因为它可能更适合@kingkupps 的回答中的情况。
我建议定义您自己的类型并让该类型实现 Error
接口。在您的测试中,您可以使用 errors.As
来确定返回的错误是新类型的实例还是包装新类型实例的错误。
package main
import (
"errors"
"fmt"
"path/filepath"
)
type ValidationError struct {
WantExtension string
GotExtension string
}
func (v ValidationError) Error() string {
return fmt.Sprintf("extension %q not accepted, please use a %s file.", v.GotExtension, v.WantExtension)
}
func Validate(fileName string) error {
fileExtension := filepath.Ext(fileName)
if fileExtension != ".vcf" {
return ValidationError{WantExtension: ".vcf", GotExtension: fileExtension}
}
return nil
}
func main() {
fileName := "something.jpg"
err := Validate(fileName)
var vErr ValidationError
if errors.As(err, &vErr) {
// you can now use vErr.WantExtension and vErr.GotExtension
fmt.Println(vErr)
fmt.Println(vErr.WantExtension)
fmt.Println(vErr.GotExtension)
}
}
打印:
extension ".jpg" not accepted, please use a .vcf file.
.vcf
.jpg
然后在您的测试中,您可以根据 Error()
的结果或它们的字段比较错误:
func TestValidate(t *testing.T) {
testCases := []struct {
name string
fileName string
expectedErr error
}{
{
name: "happy-path",
fileName: "file.vcf",
},
{
name: "wrong-extension",
fileName: "file.md",
expectedErr: ValidationError{WantExtension: ".vcf", GotExtension: ".md"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := Validate(tc.fileName)
if err != nil {
if tc.expectedErr == nil {
t.Errorf("unexpected error: %v", err)
return
}
var want ValidationError
var got ValidationError
if w, g := errors.As(tc.expectedErr, &want), errors.As(err, &got); w == g && w {
// Both errors are ValidationErrors
assertValidationErrorsEqual(t, want, got)
} else if w != g {
// The expected and actual error differ in type
t.Errorf("wanted error of type %T, got error of type %T", tc.expectedErr, err)
} else {
// Neither error is a ValidationError so we just assert that they produce the same error message
assertErrorMessagesEqual(t, tc.expectedErr, err)
}
}
})
}
}
func assertErrorMessagesEqual(t *testing.T, want error, got error) {
if w, g := want.Error(), got.Error(); w != g {
t.Errorf("want %q, got %q", w, g)
}
}
func assertValidationErrorsEqual(t *testing.T, want ValidationError, got ValidationError) {
if want.WantExtension != got.WantExtension || want.GotExtension != got.GotExtension {
t.Errorf("want %v, got %v", want, got)
}
}
编辑:感谢@Daniel Farrell 关于在类型断言上使用 errors.As
的建议。使用 errors.As
将执行您想要的操作,即使返回的错误包含 ValidationError
的实例,而类型断言不会。
编辑:添加了使用建议方法进行测试的结果。
使用errors.Is
没有问题。代码在使用它时没有失败。
是测试代码没有设置正确
在程序中,错误信息定义为Extension ".md" not accepted, please use a .vcf file.
,如return ValidationError{Msg: fmt.Sprintf("Extension %q not accepted, please use a %s file.", gotExtension, wantExtension)}
在测试中,错误信息定义为expectedErr: ValidationError{Msg: "Extension .md not accepted, please use a .vcf file."},
因此,当尝试比较两个错误值时,状态类似于
fmt.Println(
errors.Is(
ValidationError{Msg: `Extension ".md" not accepted, please use a .vcf file.`},
ValidationError{Msg: `Extension .md not accepted, please use a .vcf file.`},
),
)
使用 api 时预期 return false,因为它执行可比值的相等性检查。
https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/errors/wrap.go;l=46
供参考,大致相当于
fmt.Println(
ValidationError{Msg: `Extension ".md" not accepted, please use a .vcf file.`} ==
ValidationError{Msg: `Extension .md not accepted, please use a .vcf file.`},
)
https://go.dev/ref/spec#Comparison_operators
Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.
肯定是使用错误,这个也应该提到
Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.