I am officially crying uncle to the benevolent samaritans of Stack Overflow.
I am trying to unit test my GORM (Postgres) Fiber API using a mock DB. I have a Card
model and a CreateCardReqBody
model for the POST request body. To setup the test, I create a random CreateCardReqBody
instance, marshal it into JSON, then pass it into an *httptest.Request
. The handler uses Fiber's (*fiber.Ctx).BodyParser
function to "unmarshal" the request body into an empty Card
struct. However, when I run the test that is supposed to pass, Fiber throws an "Unprocessable Entity" error.
Below are the relevant parts of my code; the test file is a combination of this tutorial and Fiber's documentation on the (*App).Test method. (I realize the code could be cleaned up; I'm just trying to get a proof of life then focus on revising :)
I've done a few things to debug this: I've made a Postman POST request with the same values as the test and it works. Within the test itself, I marshal then unmarshal the CreateCardReqBody
struct and that works. I've triple checked the spelling of the JSON fields match, that the struct fields are exported, etc. I've also run the VSCode debugger and the body
field within Fiber.Ctx's also looks correct to me.
I'm starting to wonder if it's something with how Fiber parses the body from a test request vs. a real request. I would greatly appreciate any insight one could share on this!
Model Definition
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
}
Test file
// 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)
})
}
}
Route handler being tested
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())
}
...
}
Debugger Output
Json body when defined in test
CodePudding user response:
facepalm
I forgot to request.Header.Set("Content-Type", "application/json")
! Posting this in case it's helpful for anyone else :)