0

Working example files included at the end.

I have a package to assist in testing api handlers by creating test http contexts.

The issue is in the AssertJSONResponseBody. The problem is that once the concrete type and value are extracted from the interface, it contains a pointer to the original object. Any changes to the extracted object affect the original object. This then makes the following equal comparison useless because essentially they are pointing to the same value.

Here is the struct:

type TestRequest struct {
    Recorder *httptest.ResponseRecorder
    Context  *gin.Context
    t        *testing.T
}

func (r *TestRequest) AssertJSONResponseBody(expectedObj interface{}, mesgAndArgs ...interface{}) bool {
    outObject := reflect.Indirect(reflect.ValueOf(expectedObj)).Addr().Interface()
// Set break point at next line and compare expectedObject with outObject.
// Then, step over the line and watch the values for both objects change.
// When the decoder unmarshals the json the original object is changed
// because of the pointer in the outobject.
    err := json.NewDecoder(r.Recorder.Body).Decode(outObject)

    if err != nil {
        return assert.Error(r.t, err)
    }

    return assert.Equal(r.t, expectedObj, outObject, mesgAndArgs...)
}

How do I create a new instance of the underlying type without coupling it to the original value via a pointer?

Here are the working example files.

APIHandler/main.go

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
)

const (
    JSONBindingError   string = "An error occurred binding the request."
    EmailRequiredError string = "Email is required."
)

func main() {

    router := gin.Default()
    v1 := router.Group("/api/v1/contacts")
    {
        v1.POST("/", CreateContactHandler)
    }
    router.Run()

    fmt.Printf("hello, world\n")
}

func CreateContactHandler(c *gin.Context) {
    request := CreateContactRequest{}
    err := c.Bind(&request)
    if err != nil {
        log.Println("ERROR:", JSONBindingError, err)
        apiError := APIError{StatusCode: http.StatusBadRequest, Message: JSONBindingError}
        c.JSON(http.StatusBadRequest, apiError)
        return
    }

    if request.Contact.Email == "" {
        log.Println("ERROR:", http.StatusBadRequest, EmailRequiredError)
        apiError := APIError{StatusCode: http.StatusBadRequest, Message: EmailRequiredError}
        c.JSON(http.StatusBadRequest, apiError)
        return
    }

    // Successful client request
    // resp := h.Client.CreateContact(request)
    // c.JSON(resp.StatusCode, resp.Body)
}

type CreateContactRequest struct {
    Contact Contact
}

type Contact struct {
    Name  string
    Email string
}

type CreateContactResponse struct {
    Message string
}

type APIError struct {
    StatusCode int    `json:"status"` // Should match the response status code
    Message    string `json:"message"`
}

type APIResponse struct {
    StatusCode int
    Body       interface{}
}

APIHandler/helpers/http.go

package helpers

import (
    "bytes"
    "encoding/json"
    "net/http/httptest"
    "reflect"
    "testing"

    "github.com/stretchr/testify/assert"

    "github.com/gin-gonic/gin"
)

// TestRequest is a struct to facilitate
// HTTP Context testing with gin handlers
type TestRequest struct {
    Recorder *httptest.ResponseRecorder
    Context  *gin.Context
    t        *testing.T
}

// NewTestRequest returns a new TestRequest
func NewTestRequest(t *testing.T) *TestRequest {
    rec := httptest.NewRecorder()
    ctx, _ := gin.CreateTestContext(rec)

    ctx.Request = httptest.NewRequest("GET", "/", nil)

    return &TestRequest{
        Recorder: rec,
        Context:  ctx,
        t:        t,
    }
}

// SetJSONRequestBody returns a new TestRequest where the request is a post.
// Takes an interface to marshal into JSON and set as the request body.
func (r *TestRequest) SetJSONRequestBody(obj interface{}) *TestRequest {
    json, err := json.Marshal(obj)
    assert.NoError(r.t, err)
    r.Context.Request = httptest.NewRequest("POST", "/", bytes.NewBuffer(json))
    r.Context.Request.Header.Add("Content-Type", "application/json")
    return r
}

// AssertStatusCode asserts that the recorded status
// is the same as the submitted status.
// The message and the args are added to the message
// when the assertion fails.
func (r *TestRequest) AssertStatusCode(expectedCode int, msgAndArgs ...interface{}) bool {
    return assert.Equal(r.t, expectedCode, r.Recorder.Code, msgAndArgs...)
}

// AssertJSONResponseBody asserts that the recorded
// response body unmarshals to the given interface.
// The message and the args are added to the message
// when the assertion fails.
func (r *TestRequest) AssertJSONResponseBody(expectedObj interface{}, masgAndArgs ...interface{}) bool {
    out := reflect.Indirect(reflect.ValueOf(expectedObj)).Addr().Interface()
    err := json.NewDecoder(r.Recorder.Body).Decode(out)

    if err != nil {
        return assert.Error(r.t, err)
    }

    return assert.Equal(r.t, expectedObj, out, masgAndArgs...)
}

APIHandler/main_test.go

package main

import (
    "APIHandler/helpers"
    "net/http"
    "testing"
)

func TestSingleContactCreate(t *testing.T) {

    tests := []struct {
        toCreate        interface{}
        handlerExpected APIError
        clientReturn    APIResponse
        statusCode      int
    }{
        // when there is a JSON binding error
        {toCreate: "",
            handlerExpected: APIError{StatusCode: http.StatusBadRequest, Message: EmailRequiredError},
            clientReturn:    APIResponse{},
            statusCode:      http.StatusBadRequest},
        // when email is missing
        {toCreate: CreateContactRequest{
            Contact: Contact{
                Name: "test",
            }},
            handlerExpected: APIError{StatusCode: http.StatusBadRequest, Message: JSONBindingError},
            clientReturn:    APIResponse{},
            statusCode:      http.StatusBadRequest},
    }

    // act
    for i, test := range tests {
        req := helpers.NewTestRequest(t)
        req.SetJSONRequestBody(test.toCreate)
        CreateContactHandler(req.Context)

        // assert
        req.AssertStatusCode(test.statusCode, "Test %d", i)
        req.AssertJSONResponseBody(&test.handlerExpected, "Test %d", i)
    }
}
tarCode
  • 27
  • 2
  • 2
    *"How do I create a new instance of the underlying type without coupling it to the original value via a pointer?"* -- get the underlying type with https://golang.org/pkg/reflect/#TypeOf, get the pointed to type with [`Type.Elem()`](https://golang.org/pkg/reflect/#Type.Elem), initialize a new instance of a given type and return the pointer with https://golang.org/pkg/reflect/#New, finally get the underlying value as `interface{}` with https://golang.org/pkg/reflect/#Value.Interface – mkopriva Jun 05 '20 at 16:46
  • 1
    Please provide an understandable, standalone example of your problem. – Volker Jun 05 '20 at 16:47
  • @Volker - I've updated the post with working example files where you can debug, step through and see the issue. Thank you for all your help! – tarCode Jun 09 '20 at 17:32

1 Answers1

0

Here's the resolution per @mkopriva:

func (r *TestRequest) AssertJSONResponseBody(expectedObj interface{}, masgAndArgs ...interface{}) bool {
    elem := reflect.TypeOf(expectedObj)
    theType := elem.Elem()
    newInstance := reflect.New(theType)
    out := newInstance.Interface()

    err := json.NewDecoder(r.Recorder.Body).Decode(out)

    if err != nil {
        return assert.Error(r.t, err)
    }

    return assert.Equal(r.t, expectedObj, out, masgAndArgs...)
}
tarCode
  • 27
  • 2