unit-testing - 如何在不知道 Go 中显式类型的情况下从接口创建新对象?
问题描述
最后包含的工作示例文件。
我有一个包可以通过创建测试 http 上下文来帮助测试 api 处理程序。
问题出在 AssertJSONResponseBody 中。问题是一旦从接口中提取出具体的类型和值,它就会包含一个指向原始对象的指针。对提取对象的任何更改都会影响原始对象。然后,这使得以下相等比较无用,因为它们本质上指向相同的值。
这是结构:
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...)
}
如何在不通过指针将其耦合到原始值的情况下创建基础类型的新实例?
以下是工作示例文件。
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)
}
}
解决方案
这是每个@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...)
}
推荐阅读
- javascript - 来自谷歌工作表脚本的下拉列表,带有插入自由文本的选项
- python - 使用熊猫垂直合并熊猫数据框而不会丢失列名
- javascript - Express 无法存储 cookie
- javascript - 在 vuejs 中使用 materializecss。我如何将选项传递给 javascript 组件?
- angular - 如何将数据从一个组件传递到另一个组件
- javascript - 当一行中的所有控件都被禁用时,如何禁用表格中的按钮?
- java - 相同功能的不同版本的 SOAP 不起作用
- reactjs - 如何在表单提交时重置下拉值,其他输入值在 React Hooks 中被清除
- python - 无法从scrapy中的所有页面获取数据
- javascript - 如何将ajax响应的输出放在codeigniter的模态iframe中