I’m starting to dip my toes into the http libraries and I’m having some problems whilst testing. I’m using the httptest library to force a StatusCode, but despite constructing and returning the same error, the test believes otherwise.
package base
import (
"net/http"
"encoding/json"
"fmt"
)
// EastDevonResponse is the structured format of the response from eastdevon.gov.uk
type EastDevonResponse struct {
Label string `json:"label"`
UPRN string `json:"UPRN"`
Results string `json:"Results"`
}
// GetBinDates returns an array of EastDevonResponses which match the postcode
func GetBinDates(url string) ([]EastDevonResponse, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
non200Err := fmt.Errorf("HTTP Response Error %d", http.StatusNotFound)
return nil, non200Err
}
// Decode top level JSON array into a slice of structs
payload := make([]EastDevonResponse, 0)
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
return payload, nil
}
And the test
package base
import (
"testing"
"net/http/httptest"
"net/http"
"fmt"
)
func TestServerReturnsNon200(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
// Expect an error that matches the implementation
_, err := GetBinDates(ts.URL)
expectedErr := fmt.Errorf("HTTP Response Error %d", http.StatusNotFound)
if err != expectedErr {
t.Errorf("GetBinDates failed: got %v expected %v", err, expectedErr)
}
}
When I run the test it fails with the following error:
Edit: Just by the way, the reason that this is happening is because errors are also values. If you look at the code that errors.New uses to create a new error:
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
You can see that a new pointer to an error type is being returned, and comparing 2 errors created this way will be false in the same way that comparing this will also return false:
err1 := errors.New("I'm an error")
err2 := errors.New("I'm an error")
fmt.Println(err1 == err2)
type Object struct {
Name string
}
o1 := &Object{"obj"}
o2 := &Object{"obj"}
fmt.Println(o1 == o2)
P.s You could technically use reflect.DeepEqual to compare something like errors if the code is more complicated, but I highly doubt it will rarely need to be used
The following returns true, but I always avoid reflection if possible:
err1 := errors.New("I'm an error")
err2 := errors.New("I'm an error")
fmt.Println(reflect.DeepEqual(err1, err2))
Thank you, thats handy to know. It does feel like you’re cheating yourself out of the knowledge around the implementation with the DeepEqual though. I certainly learnt something valuable from this issue.
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
Why return a pointer ? The original object is returned anyway (not copied) and we know it’s not directly accessible because error is an interface. I’m curious to see the advantage to return object over a pointer in the case of interface.
It could be convention. It could also be that it’s a teeny tiny bit more efficient under the circumstances. We will get an interface value containing the pointer in question, which can be passed around as-is and will almost always only be used to compare against nil.
Had it returned an errorString{text} object instead, and declared the Error() method not as a pointer receiver, the interface would need to make a copy and then hold a pointer to the copy - because interface values always hold pointers to the underlying value. When passing the interface value somewhere or calling methods on it, the underlying value needs to be copied again.