You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单元测试中Go Fiber无法解析请求体问题求助

问题:Fiber单元测试中BodyParser解析请求体失败(Unprocessable Entity)

我在为基于GORM(Postgres)+Fiber的API编写单元测试,使用Mock数据库。项目中定义了Card模型和CreateCardReqBody作为POST请求体模型。测试时,我生成随机的CreateCardReqBody实例,序列化为JSON后传入*httptest.Request,但接口处理函数通过Fiber的(*fiber.Ctx).BodyParser将请求体解析到Card结构体时,Fiber抛出“Unprocessable Entity”错误。

已完成的调试操作

  • 用相同参数通过Postman发起请求可正常处理
  • 测试代码内将序列化后的JSON反序列化回CreateCardReqBody无异常
  • 反复核对JSON字段名与Card结构体字段完全匹配,且所有结构体字段已导出
  • 通过VSCode调试器确认Fiber.Ctx中的body字段内容与预期一致

我怀疑问题出在Fiber处理测试请求与真实请求的差异上,希望得到相关解决思路。


相关代码片段

模型定义

type Card struct {
	gorm.Model

	// Implicit Gorm foreign key to fleet ID
	FleetID uint `gorm:"index"  json:"fleet_id" validate:"required,min=1"`

	// Card provider's account number
	ProviderAccountNumber string `json:"provider_account_number"`

	// Card provider's external card identifier
	CardIdentifier string `gorm:"index" json:"card_identifier" validate:"min=1"`

	// Implicit Gorm foreign key to driver ID. Driver association is optional.
	DriverID uint `json:"associated_driver_id" validate:"min=1"`

	// Implicit Gorm foreign key to vehicle ID.
	VehicleID uint `json:"associated_vehicle_id" validate:"required,min=1"`

	// User-inputted start date, formatted "2020-01-26T22:38:25.000Z" in UTC
	StartDate pq.NullTime
}

测试文件

// Adapted from tutorial
type testCase struct {
	name          string
	body          CreateCardReqBody
	setupAuth     func(t *testing.T, request *http.Request)
	buildStubs    func(db *mockDB.MockDBInterface)
	checkResponse func(response *http.Response, outputErr error)
}

type CreateCardReqBody struct {
	FleetID               int    `json:"fleet_id"`
	ProviderAccountNumber string `json:"provider_account_number"`
	CardIdentifier        string `json:"card_identifier"`
	StartDate             string `json:"start_date"`
	AssociatedDriverID    int    `json:"associated_driver_id"`
	AssociatedVehicleID   int    `json:"associated_vehicle_id"`
}

func TestCreateCard(t *testing.T) {
	user := randomUser(t)
	vehicle := randomVehicle()
	driver := randomDriver(vehicle.FleetID)
	okReqCard := randomCard(vehicle.FleetID)

	finalOutputCard := okReqCard
	finalOutputCard.ID = 1

	testCases := []testCase{
		{
			name: "Ok",
			body: CreateCardReqBody{
				FleetID:               int(okReqCard.FleetID),
				ProviderAccountNumber: okReqCard.ProviderAccountNumber,
				CardIdentifier:        okReqCard.CardIdentifier,
				StartDate:             okReqCard.StartDate.Time.Format("2006-01-02T15:04:05.999Z"),
				AssociatedDriverID:    int(okReqCard.DriverID),
				AssociatedVehicleID:   int(okReqCard.VehicleID),
			},
			setupAuth: func(t *testing.T, request *http.Request) {
				addAuthorization(t, request, user)
			},
			// Tell mock database what calls to expect and what values to return
			buildStubs: func(db *mockDB.MockDBInterface) {
				db.EXPECT().
					UserExist(gomock.Eq(fmt.Sprint(vehicle.FleetID))).
					Times(1).Return(user, true, user.ID)

				db.EXPECT().
					SearchTSP(gomock.Eq(fmt.Sprint(vehicle.FleetID))).
					Times(1)

				db.EXPECT().
					SearchVehicle(gomock.Eq(fmt.Sprint(okReqCard.VehicleID))).
					Times(1).
					Return(vehicle, nil)

				db.EXPECT().
					SearchDriver(gomock.Eq(fmt.Sprint(driver.ID))).
					Times(1).
					Return(driver, nil)

				db.EXPECT().
					CardCreate(gomock.Eq(okReqCard)).
					Times(1).
					Return(finalOutputCard, nil)

			},
			checkResponse: func(res *http.Response, outputErr error) {
				require.NoError(t, outputErr)
				// Internal helper func, excluded for brevity
				requireBodyMatchCard(t, finalOutputCard, res.Body)
			},
		},
	}

	for _, test := range testCases {
		t.Run(test.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockDB := mockDB.NewMockDBInterface(ctrl)
			test.buildStubs(mockDB)

			jsonBytes, err := json.Marshal(test.body)
			require.NoError(t, err)
			jsonBody := bytes.NewReader(jsonBytes)

			// Debug check: am I able to unmarshal it back? YES.
			errUnmarsh := json.Unmarshal(jsonBytes, &CreateCardReqBody{})
			require.NoError(t, errUnmarsh)

			endpoint := "/v1/transactions/card"
			request := httptest.NewRequest("POST", endpoint, jsonBody)
			// setupAuth is helper function (not shown in this post) that adds authorization to httptest request
			test.setupAuth(t, request)
			
			app := Init("test", mockDB)
			res, err := app.Test(request)

			test.checkResponse(res, err)

		})
	}
}

被测试的路由处理函数

func (server *Server) CreateCard(c *fiber.Ctx) error {
	var card models.Card

	var err error
	// 1) Parse POST data
	if err = c.BodyParser(&card); err != nil {
		return c.Status(http.StatusUnprocessableEntity).SendString(err.Error())
	}
	...
}

调试器输出

  • 测试中定义的JSON请求体内容完整,包含所有预期字段
  • Fiber上下文内的请求体内容与测试定义的JSON完全一致

内容的提问来源于stack exchange,提问作者MarvelCoder1142

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 01:46:47