how to write a Go unit test for a function that populates a struct using external data? - function

I have a function in a handlers package that retrieves data from another function in the same package. How can write a unit test for the calling function that does not depend on calling the actual called function? The return from the function is an array of reports.
One single object in this array would look like this:
{"Timestamp": "2022-10-15 09.03.58.123456", "Resource": "arn.aws-arn-syntax", "contact":"team#company.com"}
Can I take the known signature of this data and create a "mock" array to use in the unit test? I simply want the unit test to validate that an array with this type of data has been returned.
I have looked into using interfaces to write a more decoupled function that would be easier to test, but I cannot figure out how to do that. I think an interface would be best, but if this function is to simple for an interface I am open to that.
type mystruct struct {
TimeStamp time.Time `json:"CreatedAt"`
Resource string `json:"Resource"`
Contact string `json:"Contact"`
// call another function to get the data
// data coming back is json and populate into mystruct
func getExternalReportData() (Reports []mystruct, err error)
ReportData, err := GetExternalReport() // same package has GetExternalReport
if err != nil {
err = fmt.Errorf("could not get report: %v", err)
}
for _, report := range *ReportData {
reportData := mystruct {
TimeStamp report.TimeStamp
Resource report.Resource
Contact report.Contact
}
ReportData = append(Reports, report)
}
return Reports, nil

Related

Go Fiber not able to parse body in unit test

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
Body inside Fiber context
facepalm
I forgot to request.Header.Set("Content-Type", "application/json")! Posting this in case it's helpful for anyone else :)

How to add map into the same struct which is used to deserialize json configs?

I am working on deserializing json into a struct as shown below and it works fine.
type DataConfigs struct {
ClientMetrics []Client `json:"ClientMetrics"`
}
type Client struct {
ClientId int `json:"clientId"`
.....
.....
}
const (
ConfigFile = "clientMap.json"
)
func ReadConfig(path string) (*DataConfigs, error) {
files, err := utilities.FindFiles(path, ConfigFile)
// check for error here
var dataConfig DataConfigs
body, err := ioutil.ReadFile(files[0])
// check for error here
err = json.Unmarshal(body, &dataConfig)
// check for error here
return &dataConfig, nil
}
Now I am trying to build a map of integer and Client using DataConfigs object that was created as shown above in the code. So I created a method to do the job as shown below and I modified ReadConfig method to do that too.
func ReadConfig(path string, logger log.Logger) (*DataConfigs, error) {
files, err := utilities.FindFiles(path, ConfigFile)
// check for error here
var dataConfig DataConfigs
body, err := ioutil.ReadFile(files[0])
// check for error here
err = json.Unmarshal(body, &dataConfig)
// check for error here
idx := BuildIndex(dataConfig)
// now how to add this idx and dataConfig object in one struct?
return &dataConfig, nil
}
func BuildIndex(dataConfig DataConfigs) map[int]Client {
m := make(map[int]Client)
for _, dataConfig := range dataConfig.ClientMetrics {
m[dataConfig.ClientId] = dataConfig
}
return m
}
My confusion is - Should I modify DataConfigs struct to add idx map too and then return that struct from ReadConfig method or should I create a new struct to handle that?
Basically I want to return DataConfigs struct which has ClientMetrics array along with idx map. How can I do this here? I am slightly confuse because I started with golang recently.
This is basically a design question with multiple options. First, I would avoid adding the map to your original DataConfigs type since it does not match the json representation. This could lead to confusion down the road.
Which option to choose depends on your requirements and preferences. Some ideas from the top of my head:
Have you considered returning the map only? After all, you've got every Client in your map. If you need to iterate all Clients you can iterate all values of your map.
Second option is to return the map in addition to DataConfigs. Go allows to return multiple values from a function as you already do for error handling.
Finally, you could wrap DataConfigs and your map in a new simple struct type as you already guessed.

How to deal with date fields from JSON in a GO struct

I have a JSON content, with some date fields like "resolutiondate" and "created" and "updated" as shown below
{
"expand":"names,schema",
"startAt":0,
"maxResults":50,
"total":1,
"issues":[
{
"expand":"operations,versionedRepresentations,editmeta,changelog,renderedFields",
"id":"id",
"self":"https://url1",
"key":"key1",
"fields":{
"summary":"Summary-1",
"customfield_10406":null,
"resolutiondate":"2021-06-10T10:07:35.000+0000",
"created":"2021-06-10T10:05:24.000+0000",
"description":"Description-1",
...
...
...
}
I am unmarshalling this JSON data into GO struct and saving the data in a excel sheet. Everything works as expected, the only issue is I'm defining the date fields as string datatypes in my GO struct, as below:
Resolved string `json:"resolutiondate,omitempty"`
Created string `json:"created,omitempty"`
Hence the final data saved in the excel file looks like:
But I want to save them as date datatype in the excel sheet, in a user defined format-mm/dd/yyyy. How can I effectively use the time package of Golang to achieve this ? Please help.
NOTE: I will be unable to share my complete code and the full JSON file.
For unmarshaling from custom format you need to create time.Time wrapper and implement json.Unmarshaler interface.
type CustomTime struct {
time.Time
}
func (t *CustomTime) UnmarshalJSON(b []byte) (err error) {
date, err := time.Parse(`"2006-01-02T15:04:05.000-0700"`, string(b))
if err != nil {
return err
}
t.Time = date
return
}
Now specify your time fields as CustomTime
Resolved CustomTime `json:"resolutiondate,omitempty"`
Created CustomTime `json:"created,omitempty"`
For writting into excel you need to provide more info about your implementation.
But example solution:
func (t *CustomTime) ExcelDate() string {
return t.Format("01/02/2006")
}
What you can do is, wrap string as your own custom type, and make it implement the Unmarshaler interface:
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
Then take time coming with JSON and parse it to time.Time and format with your custom layout mm/dd/yyyy.
type Date string
func (d *Date) UnmarshalJSON(bytes []byte) error {
dd, err := time.Parse(`"2006-01-02T15:04:05.000+0000"`, string(bytes))
if err != nil{
return err
}
*d = Date(dd.Format("01/02/2006"))
return nil
}
Now you can use
Created Date `json:"created,omitempty"`
inside your Go struct and unmarshal.
Run sample code here

How do I create a generic function which converts json string into a struct in golang?

First of all, I have the folowing struct:
type User struct {
Username string
Password string
FullName string
Mail string
}
And I have tried to create the folowing function:
func FromJson(emptyJsonAble interface{},jsonString string) interface{} {
err := json.Unmarshal([]byte(jsonString), &emptyJsonAble)
if err != nil {
panic(err)
}
return emptyJsonAble
}
I have called to the function in the folowing way:
user := FromJson(User{}, str)
But the function returns the folowing map instead of User struct:
map[FullName:a Mail:a Password:b Username:a]
How do I return the struct itself (witout converting the returned object every time)?
In other words, how to make the function to consider emptyJsonAble as User type when give.
I have tryed to work with reflect.Type, but I'm stuck
When you pass the User struct through, you are effectively passing it a copy of the value, in your scenario what you want to do is pass a pointer reference to the type. Then, because you already have a pointer, you do not need to pass by reference inside of your function:
user := FromJson(&User{}, str)
err := json.Unmarshal([]byte(jsonString), emptyJsonAble)
https://play.golang.org/p/uXMcFCl138y
UPDATE:
equally, because you are now passing by reference, you do not need to return a copy of the unmarshalled data struct:
https://play.golang.org/p/GJKqVJLbRCZ
In both cases, we can see the results are the same, so your return of the data is unnecessary

Golang slices of struct or newbie trouble building REST

and need your help.
Wanted to build simple api and stuck with some problem.
I've choose gin and database/sql with postgres driver
package main
import (
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)
func main() {
router := gin.Default()
router.GET("/search/:text", SearchWord)
router.Run(":8080")
}
I need to make query to DB and make json out of this request.
func checkErr(err error) {
if err != nil {
panic(err)
}
}
type Message struct {
ticket_id int `json:"ticket_id"`
event string `json:"event"`
}
func SearchWord(c *gin.Context) {
word := c.Params.ByName("text")
db, err := sql.Open("postgres", "host=8.8.8.8 user= password= dbname=sample")
defer db.Close()
checkErr(err)
rows, err2 := db.Query("SELECT ticket_id,event FROM ....$1, word)
checkErr(err)
for rows.Next() {
var ticket_id int
var event string
err = rows.Scan(&ticket_id, &event)
checkErr(err)
fmt.Printf("%d | %s \n\n", ticket_id, event)
}
}
This coda working nice, but when i need to make json.
I need to make struct of a row
type Message struct {
ticket_id int `json:"ticket_id"`
event string `json:"event"`
}
an then i need to create slice , and append every rows.Next() loop an than answer to browser with Json...
c.JSON(200, messages)
But how to do that...don't know :(
disclaimer: I am brand new to go
Since you Scanned your column data into your variables, you should be able to initialize a structure with their values:
m := &Message{ticket_id: ticket_id, event: event}
You could initialize a slice with
s := make([]*Message, 0)
And then append each of your message structs after instantiation:
s = append(s, m)
Because I'm not too familiar with go there are a couple things i'm not sure about:
after copying data from query to your vars using rows.Scan does initializing the Message struct copy the current iterations values as expected??
If there is a way to get the total number of rows from your query it might be slighlty more performant to initialize a static length array, instead of a slice?
I think #inf deleted answer about marshalling your Message to json down the line might need to be addressed, and Message field's might need to be capitalized
copied from #inf:
The names of the members of your struct need be capitalized so that
they get exported and can be accessed.
type Message struct {
Ticket_id int `json:"ticket_id"`
Event string `json:"event"` }
I'm going to cheat a little here and fix a few things along the way:
First: open your database connection pool once at program start-up (and not on every request).
Second: we'll use sqlx to make it easier to marshal our database rows into our struct.
package main
var db *sqlx.DB
func main() {
var err error
// sqlx.Connect also checks that the connection works.
// sql.Open only "establishes" a pool, but doesn't ping the DB.
db, err = sqlx.Connect("postgres", "postgres:///...")
if err != nil {
log.Fatal(err)
}
router := gin.Default()
router.GET("/search/:text", SearchWord)
router.Run(":8080")
}
// in_another_file.go
type Message struct {
TicketID int `json:"ticket_id" db:"ticket_id"`
Event string `json:"event" db:"event"`
}
func SearchWord(c *gin.Context) {
word := c.Params.ByName("text")
// We create a slice of structs to marshal our rows into
var messages []*Message{}
// Our DB connection pool is safe to use concurrently from here
err := db.Select(&messages, "SELECT ticket_id,event FROM ....$1, word)
if err != nil {
http.Error(c.Writer, err.Error(), 500)
return
}
// Write it out using gin-gonic's JSON writer.
c.JSON(200, messages)
}
I hope that's clear. sqlx also takes care of calling rows.Close() for you, which will otherwise leave connections hanging.