如何在惯用的 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.