1

I'm currently writing some unit tests for a package in which a function can return several types of errors. I've defined the struct as:

tests := []struct {
    name   string
    data   string
    url    string
    status int
}

And would like to use errors.As() to find test.err within the error I test upon. An example struct that I used within my tests is for example:

{
    name:   "url not available",
    err:    &url.Error{},
    data:   srvData,
    url:    "a",
    status: http.StatusOK,
},

I want to use errors.As for different struct types that implement the error interface. Hence as you can see within the struct I have defined err as error. As can be seen I use &url.Error{} which should be implementing the error interface.

t.Run(test.name, func(t *testing.T) {
    data, err := i.getID(url)
    if err != nil {
        require.NotNil(t, test.err)
        assert.True(t, errors.As(err, &test.err))
    } else {
        // ...
    }
})

However, using errors.As as above returns

second argument to errors.As should not be *error

Now to my understanding errors.As() accepts any as second argument, so I'm confused as to why I can't use *error.

I have also tried changing the err field within the test struct to interface{} instead; however doing so made all assertion pass, regardless of whether the target was present within the error.

I couldn't find a solution on how to use errors.As() for different types that implement the error interface in a similar fashion as above, so for now I rely on using Contains() instead. Was wondering if anyone could provide some insight.

Vy Do
  • 46,709
  • 59
  • 215
  • 313
  • 1
    Using `*error` doesn't do anything because all errors are already and `error` (see https://stackoverflow.com/questions/75489773/why-do-i-get-second-argument-to-errors-as-should-not-be-error-build-error-in), so you would just get back the same thing. If you want to exactly match and assign an error to `*url.Error`, you need to pass `**url.Error`. – JimB Apr 26 '23 at 12:40
  • Thanks, could you elaborate? Instead of passing &test.err as the second argument I first assign it to a variable and then get the pointer of that. Indeed I think the issue is that i'm "casting" the actual struct that implements the error interface to be read as the "interface" itself, hence the complaint that it is already an error – anthonyoliai Apr 26 '23 at 13:01

1 Answers1

0

A pointer to an error type does not satisfy the error interface, which is why the second argument to As is of type any. In order to store the type you want directly in your .err field, that field will also have to be of type any.

However because you have wrapped this pointer value in an interface, you will need to use a type assertion or reflection to get that value back out for inspection:

var testErr any = new(*url.Error)
_, err := http.Get("http://error.error/")

if errors.As(err, testErr) {
    fmt.Println(reflect.ValueOf(testErr).Elem())
}
JimB
  • 104,193
  • 13
  • 262
  • 255
  • Great! That worked out. What confuses me is why using new() within the test struct works while defining `err: &time.ParseError{},` and calling `assert.True(t, errors.As(err, &test.err))` does not work. Wouldnt it in both cases be a pointer to a pointer? – anthonyoliai Apr 26 '23 at 13:51
  • `&test.err` is _not_ a pointer to a pointer, it is a pointer to an interface (an `*error`, which happens to contain a pointer, but that is not part of the type being passed). When you use `new(*url.Error)` you are assigning a `**url.Error` value, which is then passed directly to `As`. – JimB Apr 26 '23 at 13:58