I would like to write a unit test for a controller but I keep getting a runtime error. I found out that it is due to the absence of the Host on the request, the ClientIP() method and request body. How can I set them on the test context?
Here is what I got so far. It fails on the line with Host: c.Request.Host.
Controller:
type loggingControllers struct {
LoggingService services.InterfaceLogging
}
func (l *loggingControllers) RegisterError(c *gin.Context) {
errorEvent := models.Error{
Badges: map[string]string{},
Host: c.Request.Host,
ClientIP: c.ClientIP(),
UserAgent: c.Request.UserAgent(),
Count: 1,
Timestamp: time.Now().Unix(),
}
err := json.NewDecoder(c.Request.Body).Decode(&errorEvent)
if err != nil {
utils.RespondWithError(c, http.StatusInternalServerError, err.Error())
return
}
go l.LoggingService.SaveError(errorEvent)
utils.RespondWithSuccess(c)
}
func GetLoggingControllerMock() loggingControllers {
loggingServiceMock := services.GetLoggingServiceMock()
return loggingControllers{
LoggingService: &loggingServiceMock,
}
}
Unit Test:
func TestLoggingControllers(t *testing.T) {
loggingControllers := GetLoggingControllerMock()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
loggingControllers.RegisterError(c)
}
Error Message:
--- FAIL: TestLoggingControllers (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x38 pc=0x15238ec]
Thanks to Adrian's comment I found the solution.
func TestLoggingControllers(t *testing.T) {
loggingControllers := GetLoggingControllerMock()
w := httptest.NewRecorder()
c, r := gin.CreateTestContext(w)
r.POST("/", loggingControllers.RegisterError)
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBuffer([]byte("{}")))
r.ServeHTTP(w, c.Request)
if w.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
}
}
Related
I want to unit test gin controller function.
package controllers
import (
"fmt"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/go-playground/assert/v2"
)
func TestGetDivisions(t *testing.T) {
tt := []struct {
Name string
EnterpriseID string
Code int
}{
{Name: "Enterprise id exists", EnterpriseID: "1", Code: 200},
{Name: "Enterprise id cannot be string", EnterpriseID: "string", Code: 404},
}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
for _, tc := range tt {
c.Params = []gin.Param{
{
Key: "enterprise_id",
Value: tc.EnterpriseID,
},
}
GetDivisions(c) // Error from here
assert.Equal(t, tc.Code, w.Code, tc.Name)
}
}
Problem is, whenever I use GetDivision function inside a test file gives error like below:
--- FAIL: TestGetDivisions (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x43c7438]
Cannot find where is a problem. Can I give test context to a function as a parameter?
And here is my GetDivisions handler:
// Get divisions of an enterprise by given id. Default id = "1"
func GetDivisions(c *gin.Context) {
var divisions []models.Division
if err := config.DB.Find(&divisions).Error; err != nil {
c.JSON(http.StatusInternalServerError, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{
"divisions": divisions,
})
}
It seems like an error occurs in config.DB.Find(&divisions), because I think invalid memory error accurs when it reads from database and writes to variable... can you show the code of config.DB.Find(). (if it is on GitHub, you can send me link)
I have a controller function like this....
func GetMaterialByFilter(c *gin.Context) {
queryParam := weldprogs.QueryParam{}
c.BindQuery(&queryParam)
materialByFilter, getErr := services.WeldprogService.GetMaterialByFilter(&queryParam)
if getErr != nil {
//TODO : Handle user creation error
c.JSON(getErr.Status, getErr)
return
}
c.JSON(http.StatusOK, materialByFilter)
}
QueryParam Struct is like this..
type QueryParam struct {
Basematgroup_id []string `form:"basematgroup_id"`
License_id []string `form:"license_id"`
Diameter_id []string `form:"diameter_id"`
Gasgroup_id []string `form:"gasgroup_id"`
Wiregroup_id []string `form:"wiregroup_id"`
Wiremat_id []string `form:"wiremat_id"`
}
My test function is like this..
func TestGetMaterialByFilter(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
GetMaterialByFilter(c)
assert.Equal(t, 200, w.Code)
var got gin.H
err := json.Unmarshal(w.Body.Bytes(), &got)
if err != nil {
t.Fatal(err)
}
fmt.Println(got)
assert.Equal(t, got, got)
}
On running this test it is giving me the following error
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x97f626]
But when i comment out the c.BindQuery() line in my controller function it successfully run my test function. What i am doing wrong here? can i somehow mock the c.BindQuery function?
To test operations that involve the HTTP request, you have to actually initialize an *http.Request and set it to the Gin context. To specifically test c.BindQuery it's enough to properly initialize the request's URL and URL.RawQuery:
func mockGin() (*gin.Context, *httptest.ResponseRecorder) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// test request, must instantiate a request first
req := &http.Request{
URL: &url.URL{},
Header: make(http.Header), // if you need to test headers
}
// example: req.Header.Add("Accept", "application/json")
// request query
testQuery := weldprogs.QueryParam{/* init fields */}
q := req.URL.Query()
for _, s := range testQuery.Basematgroup_id {
q.Add("basematgroup_id", s)
}
// ... repeat for other fields as needed
// must set this, since under the hood c.BindQuery calls
// `req.URL.Query()`, which calls `ParseQuery(u.RawQuery)`
req.URL.RawQuery = q.Encode()
// finally set the request to the gin context
c.Request = req
return c, w
}
If you need to mock JSON binding, see this answer.
The service call services.WeldprogService.GetMaterialByFilter(&queryParam) can't be tested as is. To be testable it has to be (ideally) an interface and somehow injected as dependency of your handler.
Assuming that it is already an interface, to make it injectable, you either require it as an handler argument — but this forces you to change the signature of the handler —, or you set it as a Gin context value:
func GetMaterialByFilter(c *gin.Context) {
//...
weldprogService := mustGetService(c)
materialByFilter, getErr := weldprogService.GetMaterialByFilter(&queryParam)
// ...
}
func mustGetService(c *gin.Context) services.WeldprogService {
svc, exists := c.Get("svc_context_key")
if !exists {
panic("service was not set")
}
return svc.(services.WeldprogService)
}
Then you can mock it in your unit tests:
type mockSvc struct {
}
// have 'mockSvc' implement the interface
func TestGetMaterialByFilter(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// now you can set mockSvc into the test context
c.Set("svc_context_key", &mockSvc{})
GetMaterialByFilter(c)
// ...
}
I have a controller function like this....
func GetMaterialByFilter(c *gin.Context) {
queryParam := weldprogs.QueryParam{}
c.BindQuery(&queryParam)
materialByFilter, getErr := services.WeldprogService.GetMaterialByFilter(&queryParam)
if getErr != nil {
//TODO : Handle user creation error
c.JSON(getErr.Status, getErr)
return
}
c.JSON(http.StatusOK, materialByFilter)
}
QueryParam Struct is like this..
type QueryParam struct {
Basematgroup_id []string `form:"basematgroup_id"`
License_id []string `form:"license_id"`
Diameter_id []string `form:"diameter_id"`
Gasgroup_id []string `form:"gasgroup_id"`
Wiregroup_id []string `form:"wiregroup_id"`
Wiremat_id []string `form:"wiremat_id"`
}
My test function is like this..
func TestGetMaterialByFilter(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
GetMaterialByFilter(c)
assert.Equal(t, 200, w.Code)
var got gin.H
err := json.Unmarshal(w.Body.Bytes(), &got)
if err != nil {
t.Fatal(err)
}
fmt.Println(got)
assert.Equal(t, got, got)
}
On running this test it is giving me the following error
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x97f626]
But when i comment out the c.BindQuery() line in my controller function it successfully run my test function. What i am doing wrong here? can i somehow mock the c.BindQuery function?
To test operations that involve the HTTP request, you have to actually initialize an *http.Request and set it to the Gin context. To specifically test c.BindQuery it's enough to properly initialize the request's URL and URL.RawQuery:
func mockGin() (*gin.Context, *httptest.ResponseRecorder) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// test request, must instantiate a request first
req := &http.Request{
URL: &url.URL{},
Header: make(http.Header), // if you need to test headers
}
// example: req.Header.Add("Accept", "application/json")
// request query
testQuery := weldprogs.QueryParam{/* init fields */}
q := req.URL.Query()
for _, s := range testQuery.Basematgroup_id {
q.Add("basematgroup_id", s)
}
// ... repeat for other fields as needed
// must set this, since under the hood c.BindQuery calls
// `req.URL.Query()`, which calls `ParseQuery(u.RawQuery)`
req.URL.RawQuery = q.Encode()
// finally set the request to the gin context
c.Request = req
return c, w
}
If you need to mock JSON binding, see this answer.
The service call services.WeldprogService.GetMaterialByFilter(&queryParam) can't be tested as is. To be testable it has to be (ideally) an interface and somehow injected as dependency of your handler.
Assuming that it is already an interface, to make it injectable, you either require it as an handler argument — but this forces you to change the signature of the handler —, or you set it as a Gin context value:
func GetMaterialByFilter(c *gin.Context) {
//...
weldprogService := mustGetService(c)
materialByFilter, getErr := weldprogService.GetMaterialByFilter(&queryParam)
// ...
}
func mustGetService(c *gin.Context) services.WeldprogService {
svc, exists := c.Get("svc_context_key")
if !exists {
panic("service was not set")
}
return svc.(services.WeldprogService)
}
Then you can mock it in your unit tests:
type mockSvc struct {
}
// have 'mockSvc' implement the interface
func TestGetMaterialByFilter(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// now you can set mockSvc into the test context
c.Set("svc_context_key", &mockSvc{})
GetMaterialByFilter(c)
// ...
}
I am trying to unit test a function which uses https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#S3.ListObjectsV2Pages to list contents of a given s3 bucket.
In my main.go file I have this function:
func listS3Objects(s3Bucket string, s3Prefix string, svc s3iface.S3API) ([]string, error) {
input := &s3.ListObjectsV2Input{
Bucket: aws.String(s3Bucket),
Prefix: aws.String(s3Prefix),
}
var s3Items []string
err := svc.ListObjectsV2Pages(input, func(resp *s3.ListObjectsV2Output, lastPage bool) bool {
for _, s3Item := range resp.Contents {
s3Items = append(s3Items, *s3Item.Key)
}
return true
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
return nil, awsErr
}
return nil, err
}
return s3Items, nil
}
In my main_test.go file I have:
type mockS3Client struct {
s3iface.S3API
}
func (m *mockS3Client) ListObjectsV2Pages(input *s3.ListObjectsV2Input, fn func(*s3.ListObjectsV2Output, bool) bool) error {
var s3Output s3.ListObjectsV2Output
var s3Object s3.Object
var s3Objects []*s3.Object
s3Object.SetKey(*input.Bucket)
s3Objects = append(s3Objects, &s3Object)
s3Output.Contents = s3Objects
fn(&s3Output, true)
return nil
}
The test looks like below:
func TestListS3Objects(t *testing.T) {
testBucket := "testBucket"
testPrefix := "testPrefix"
var mockSvc mockS3Client
s3Items, _ := listS3Objects(testBucket, testPrefix, mockSvc.S3API)
if len(s3Items) != 1 {
t.Errorf("Expected '%v' elements but got '%v'", 1, len(s3Items))
}
if s3Items[0] != testBucket {
t.Errorf("Expected '%v' value but got '%v'", testBucket, s3Items[0])
}
}
This test generates below stack trace:
--- FAIL: TestListS3Objects (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x5a0 pc=0x797066]
goroutine 7 [running]:
testing.tRunner.func1(0xc00017e300)
/usr/local/go/src/testing/testing.go:874 +0x3a3
panic(0x800c20, 0xc657c0)
/usr/local/go/src/runtime/panic.go:679 +0x1b2
github.com/AirHelp/business-metrics-restore.listS3Objects(0x88d6aa, 0xa, 0x88d6b4, 0xa, 0x0, 0x0, 0x1, 0xc000018dd8, 0x44f278, 0x5aa38b1b10f7, ...)
/workspaces/business-metrics-restore/bmr.go:189 +0x126
github.com/AirHelp/business-metrics-restore.TestListS3Objects(0xc00017e300)
/workspaces/business-metrics-restore/bmr_test.go:59 +0x65
testing.tRunner(0xc00017e300, 0x8a7fa8)
/usr/local/go/src/testing/testing.go:909 +0xc9
created by testing.(*T).Run
/usr/local/go/src/testing/testing.go:960 +0x350
FAIL github.com/AirHelp/business-metrics-restore 0.044s
FAIL
Error: Tests failed.
I am under the impression there may be some problem with my mocked version of ListObjectsV2Pages but am not able to find the root cause. I tried to keep this mocked function as simple as possible...
Can someone help me?
Thanks!
Best Regards,
Rafal.
how i did it was i embedded s3iface.S3API in a new struct in my application code, and added a slice to save s3items, then provided a separate paginator function
type myS3 struct {
s3iface.S3API
s3items []*s3Item.Key
}
func (s *myS3) findObjects(bucket, prefix string) error {
err := s.ListObjectsV2Pages(&s3.ListObjectsV2Input{
Bucket: &bucket,
Prefix: &prefix,
}, s.Paginator)
return err
}
func (s *myS3) paginator(page *s3.ListObjectsV2Output, lastPage bool) bool {
for _, content := range page.Contents {
s.s3items = append(s.s3items, *content.Key)
}
return lastPage
}
this way, you will only need to test if paginator is filling the s3items field correctly, and to see if whatever function is calling findObjects is handling errors correctly
I'm setting up testing in Go.
I use go-sqlmock to test mysql connection and Go Gin as framework. Now I try to test mysql insert logic.
The problem is I need to set mock gin.Context which is used for BindJSON later.
But I can't set this gin.Context so far.
server side: golang
db: mysql
web framework: gin
dao.go
unc PostImageToDBDao(c *gin.Context, db *sql.DB) {
// Because of this logic, I need to set gin.Context with json
var imageData util.ImageData
c.BindJSON(&imageData)
for _, imageName := range imageData.IMAGENAMES {
ins, err := db.Prepare("INSERT INTO images(article_uuid, image_name) VALUES(?,?)")
if err != nil {
log.Fatal(err)
}
ins.Exec(imageData.ARTICLEUUID, imageName.NAME)
}
}
dao_test.go
func TestPostImageToDBDao(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
prep := mock.ExpectPrepare("^INSERT INTO images*")
prep.ExpectExec().
WithArgs("bea1b24d-0627-4ea0-aa2b-8af4c6c2a41c", "b8119536-fad5-4ffa-ab71-2f96cca19697").
WillReturnResult(sqlmock.NewResult(1, 1))
// try to set context with json post
req, _ := http.NewRequest("POST", "/post/image/db", bytes.NewBuffer([]byte(`[{
"articleUUID": "bea1b24d-0627-4ea0-aa2b-8af4c6c2a41c",
"imageNames": "b8119536-fad5-4ffa-ab71-2f96cca19697",
}]`)))
req.Header.Set("Content-Type", "application/json")
var context *gin.Context
context = &gin.Context{Request: req}
PostImageToDBDao(context, db)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expections: %s", err)
}
}
I expect mock gin.Context to set properly and run go test -v without error, however it fails with the following error:
=== RUN TestPostImageToDBDao
[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 0 with 400
--- FAIL: TestPostImageToDBDao (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x60 pc=0x15bde75]
goroutine 50 [running]:
testing.tRunner.func1(0xc000234100)
/usr/local/go/src/testing/testing.go:830 +0x392
panic(0x16918e0, 0x1ce5850)
/usr/local/go/src/runtime/panic.go:522 +0x1b5
github.com/gin-gonic/gin.(*Context).AbortWithStatus(0xc00026aee8, 0x190)
/Users/jpskgc/go/pkg/mod/github.com/gin-gonic/gin#v1.4.0/context.go:146 +0x45
github.com/gin-gonic/gin.(*Context).AbortWithError(0xc0000d9ee8, 0x190, 0x1863e00, 0xc0002700a0, 0x1863e00)
/Users/jpskgc/go/pkg/mod/github.com/gin-gonic/gin#v1.4.0/context.go:162 +0x39
github.com/gin-gonic/gin.(*Context).MustBindWith(0xc00026aee8, 0x16328e0, 0xc00022c180, 0x186e060, 0x1d16588, 0x1e316d0, 0x0)
/Users/jpskgc/go/pkg/mod/github.com/gin-gonic/gin#v1.4.0/context.go:561 +0x92
github.com/gin-gonic/gin.(*Context).BindJSON(...)
/Users/jpskgc/go/pkg/mod/github.com/gin-gonic/gin#v1.4.0/context.go:528
article/api/dao.PostImageToDBDao(0xc00026aee8, 0xc000276000)
/Users/jpskgc/article/api/dao/dao.go:54 +0x87
article/api/dao.TestPostImageToDBDao(0xc000234100)
/Users/jpskgc/article/api/dao/dao_test.go:204 +0x4b6
testing.tRunner(0xc000234100, 0x17897e0)
/usr/local/go/src/testing/testing.go:865 +0xc0
created by testing.(*T).Run
/usr/local/go/src/testing/testing.go:916 +0x35a
exit status 2
FAIL article/api/dao 0.032s
First, you must instantiate a test *gin.Context and make sure its *http.Request is non-nil:
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = &http.Request{
Header: make(http.Header),
}
Then you can mock a POST json body in the following way:
func MockJsonPost(c *gin.Context /* the test context */, content interface{}) {
c.Request.Method = "POST" // or PUT
c.Request.Header.Set("Content-Type", "application/json")
jsonbytes, err := json.Marshal(content)
if err != nil {
panic(err)
}
// the request body must be an io.ReadCloser
// the bytes buffer though doesn't implement io.Closer,
// so you wrap it in a no-op closer
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonbytes))
}
where the function argument content interface{} is anything that can be marshalled into JSON with json.Marshal(), so in most cases a struct with the proper json tags, or a map[string]interface{}.
Example usage:
func TestMyHandler(t *testing.T) {
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
Header: make(http.Header),
}
MockJsonPost(ctx, map[string]interface{}{"foo": "bar"})
MyHandler(ctx)
assert.EqualValues(t, http.StatusOK, w.Code)
}
Related:
How to unit test a Go Gin handler function?