I'm building an aws lambda using aws-sdk-go and aws-lambda-go and I'm stuck with a little problem.
I want to test my lambda handler. To do so, I need to mock a valid context.Context containing valid attributes for lamdacontext.LambdaContext and satisfy lambdacontext.FromContext.
I cannot seem to find a way to build such mock, since lambdacontext.FromContext always returns me _, false.
Here's my main, with a simple handler on a events.SNSEvent event:
package main
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-lambda-go/lambdacontext"
)
func main() {
lambda.Start(handleRequest)
}
func handleRequest(ctx context.Context, snsEvent events.SNSEvent) error {
lc, ok := lambdacontext.FromContext(ctx); if !ok {
// Always false
...
return someErr
}
. . .
return nil
}
And here's my test for handleRequest:
package main
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambdacontext"
"github.com/stretchr/testify/assert"
"gitlab.easy-network.it/meg/aml-rekognition/testdata"
"testing"
)
const imgMock = `
{
\"some_parameter\": \"some_value\"
}`
func TestHandleRequest(t *testing.T) {
c := context.Background()
ctxV := context.WithValue(c, "", map[string]interface{}{
"AwsRequestID" : "some_aws_id",
"InvokedFunctionArn" : "some_arn",
"Identity" : lambdacontext.CognitoIdentity{},
"ClientContext" : lambdacontext.ClientContext{},
})
snsEventMock := events.SNSEvent{
Records: []events.SNSEventRecord{
{
SNS: events.SNSEntity{
Message: imgMock,
},
},
},
}
err := handleRequest(ctxV, snsEventMock)
assert.NoError(t, err)
}
I also tried other mocks like passing it a struct with these parameters etc, but I always get false. For instance, I tried also:
type TestMock struct {
AwsRequestID string
InvokedFunctionArn string
Identity lambdacontext.CognitoIdentity
ClientContext lambdacontext.ClientContext
}
func TestHandleRequest(t *testing.T) {
c := context.Background()
testMock := TestMock{
AwsRequestID : "some_aws_id",
InvokedFunctionArn : "some_arn",
Identity : lambdacontext.CognitoIdentity{},
ClientContext : lambdacontext.ClientContext{},
}
ctxV := context.WithValue(c, "", testMock)
. . .
}
I checked out the source of FromContext and I've been scratching my head for a while.
// LambdaContext is the set of metadata that is passed for every Invoke.
type LambdaContext struct {
AwsRequestID string
InvokedFunctionArn string
Identity CognitoIdentity
ClientContext ClientContext
}
// An unexported type to be used as the key for types in this package.
// This prevents collisions with keys defined in other packages.
type key struct{}
// The key for a LambdaContext in Contexts.
// Users of this package must use lambdacontext.NewContext and
lambdacontext.FromContext
// instead of using this key directly.
var contextKey = &key{}
// FromContext returns the LambdaContext value stored in ctx, if any.
func FromContext(ctx context.Context) (*LambdaContext, bool) {
lc, ok := ctx.Value(contextKey).(*LambdaContext)
return lc, ok
}
Of course, it returns false even if I just pass a context.Background() to it.
Any idea on how should I build a valid context.Context to let lambdacontext.FromContext return true?
lambda.FromContext() checks if the passed context.Context contains a value with a "private" key hold inside the lambdacontext package:
// An unexported type to be used as the key for types in this package.
// This prevents collisions with keys defined in other packages.
type key struct{}
// The key for a LambdaContext in Contexts.
// Users of this package must use lambdacontext.NewContext and lambdacontext.FromContext
// instead of using this key directly.
var contextKey = &key{}
// FromContext returns the LambdaContext value stored in ctx, if any.
func FromContext(ctx context.Context) (*LambdaContext, bool) {
lc, ok := ctx.Value(contextKey).(*LambdaContext)
return lc, ok
}
You cannot access this key, and you can't "reproduce" it (you can't create a value that will be equal to this "private" key).
But there's an easy way, simply use the lambdacontext.NewContext() to derive a context which will have this key:
// NewContext returns a new Context that carries value lc.
func NewContext(parent context.Context, lc *LambdaContext) context.Context {
return context.WithValue(parent, contextKey, lc)
}
So the solution:
ctx := context.Background()
// Add keys to your liking, then:
lc := new(lambdacontext.LambdaContext)
ctx = lambdacontext.NewContext(ctx, lc)
Related
I'm validating my config file using Go's validator package
One of the fields(createTime) from the config file I'm reading as a string, and want to make sure that it is a valid time duration:
type Config struct{
...
CreateTime string `yaml:"createTime" validate:"duration,required"`
...
}
So, I have written a custom validation function as follows:
import (
"time"
"github.com/go-playground/validator/v10"
)
// isValidDuration is a custom validation function, and it will check if the given string is a valid time duration
func isValidDuration(fl validator.FieldLevel) bool {
if _, err := time.ParseDuration(fl.Field().String()); err != nil {
return false
}
return true
}
Which I'm using for validation as follows:
func (configObject *Config) Validate() error {
// Validate configurations
validate := validator.New()
if err := validate.RegisterValidation("duration", isValidDuration); err != nil {
return err
}
return validate.Struct(configObject)
}
The custom validator is working fine and I want to write a unit test for isValidDuration function. Following is the unit test boilerplate generated by IDE:
import (
"testing"
"github.com/go-playground/validator/v10"
)
func Test_isValidDuration(t *testing.T) {
type args struct {
fl validator.FieldLevel
}
var tests = []struct {
name string
args args
want bool
}{
// TODO: Add test cases.
{name: "positive", args: ???????, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isValidDuration(tt.args.fl); got != tt.want {
t.Errorf("isValidDuration() = %v, want %v", got, tt.want)
}
})
}
}
I'm new to Go and not sure what to pass in the args field of the testcase above. How do I create a struct containing one validator.FieldLevel field?
Ideally, I would like to pass something like "10m" as args and since it is a valid time duration, would expect the output of isValidDuration as true since "10m" is a valid duration.
I'm trying this: {name: "positive", args: struct{ fl validator.FieldLevel }{fl: "10m"}, want: true} but getting this ereor: '"10m"' (type string) cannot be represented by the type validator.FieldLevel
How do I create a validator.FieldLevel variable with value equivalent to "10m"? Could someone please help me out?
problem1
A language semantic problem, fl is not the type String.
{
name: "positive",
args: args{
fl: validator.FieldLevel{
// ....
},
},
want: true,
},
problem2
How to use validator.FieldLevel.
In code base, we can see FieldLevel is a interface, and you need create struct validate, which is not exported and not supposed to be used by user.
type FieldLevel interface
// ...
var _ FieldLevel = new(validate)
answer
So, you'd best to write your UT like UT of validator package. Don't use the code from your IDE!
// have a look at this UT
// github.com/go-playground/validator/v10#v10.10.0/validator_test.go
func TestKeysCustomValidation(t *testing.T) {
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)
// ...
}
This question already has answers here:
How do you create a new instance of a struct from its type at run time in Go?
(5 answers)
Closed 9 months ago.
Working example files included at the end.
I have a package to assist in testing api handlers by creating test http contexts.
The issue is in the AssertJSONResponseBody. The problem is that once the concrete type and value are extracted from the interface, it contains a pointer to the original object. Any changes to the extracted object affect the original object. This then makes the following equal comparison useless because essentially they are pointing to the same value.
Here is the struct:
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...)
}
How do I create a new instance of the underlying type without coupling it to the original value via a pointer?
Here are the working example files.
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)
}
}
Here's the resolution per #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...)
}
I'm writing a REST API using Gin framework. But I was faced a trouble testing my controllers and researching TDD and Mock. I tried to apply TDD and Mock to my code but I could not.
I created a very reduced test environment and tried to create a controller test. How do I create a Mock for Gin.Context?
Here's my example code:
package main
import (
"strconv"
"github.com/gin-gonic/gin"
)
// MODELS
type Users []User
type User struct {
Name string `json"name"`
}
func main() {
r := gin.Default()
r.GET("/users", GetUsers)
r.GET("/users/:id", GetUser)
r.Run(":8080")
}
// ROUTES
func GetUsers(c *gin.Context) {
repo := UserRepository{}
ctrl := UserController{}
ctrl.GetAll(c, repo)
}
func GetUser(c *gin.Context) {
repo := UserRepository{}
ctrl := UserController{}
ctrl.Get(c, repo)
}
// CONTROLLER
type UserController struct{}
func (ctrl UserController) GetAll(c *gin.Context, repository UserRepositoryIterface) {
c.JSON(200, repository.GetAll())
}
func (ctrl UserController) Get(c *gin.Context, repository UserRepositoryIterface) {
id := c.Param("id")
idConv, _ := strconv.Atoi(id)
c.JSON(200, repository.Get(idConv))
}
// REPOSITORY
type UserRepository struct{}
type UserRepositoryIterface interface {
GetAll() Users
Get(id int) User
}
func (r UserRepository) GetAll() Users {
users := Users{
{Name : "Wilson"},
{Name : "Panda"},
}
return users
}
func (r UserRepository) Get(id int) User {
users := Users{
{Name : "Wilson"},
{Name : "Panda"},
}
return users[id-1]
}
My test example:
package main
import(
"testing"
_ "github.com/gin-gonic/gin"
)
type UserRepositoryMock struct{}
func (r UserRepositoryMock) GetAll() Users {
users := Users{
{Name : "Wilson"},
{Name : "Panda"},
}
return users
}
func (r UserRepositoryMock) Get(id int) User {
users := Users{
{Name : "Wilson"},
{Name : "Panda"},
}
return users[id-1]
}
// TESTING REPOSITORY FUNCTIONS
func TestRepoGetAll(t *testing.T) {
userRepo := UserRepository{}
amountUsers := len(userRepo.GetAll())
if amountUsers != 2 {
t.Errorf("Esperado %d, recebido %d", 2, amountUsers)
}
}
func TestRepoGet(t *testing.T) {
expectedUser := struct{
Name string
}{
"Wilson",
}
userRepo := UserRepository{}
user := userRepo.Get(1)
if user.Name != expectedUser.Name {
t.Errorf("Esperado %s, recebido %s", expectedUser.Name, user.Name)
}
}
/* HOW TO TEST CONTROLLER?
func TestControllerGetAll(t *testing.T) {
gin.SetMode(gin.TestMode)
c := &gin.Context{}
c.Status(200)
repo := UserRepositoryMock{}
ctrl := UserController{}
ctrl.GetAll(c, repo)
}
*/
Gin provides the option to create a Test Context which you can use for whatever you need:
https://godoc.org/github.com/gin-gonic/gin#CreateTestContext
Like that:
c, _ := gin.CreateTestContext(httptest.NewRecorder())
Here is an example of how I mock a context, add a param, use it in a function, then print the string of the response if there was a non-200 response.
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = []gin.Param{gin.Param{Key: "k", Value: "v"}}
foo(c)
if w.Code != 200 {
b, _ := ioutil.ReadAll(w.Body)
t.Error(w.Code, string(b))
}
In order to get a *gin.Context instance that you can test, you need a mock HTTP request and response. An easy way to create those is to use the net/http and net/http/httptest packages. Based on the code you linked, your test would look like this:
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestControllerGetAll(t *testing.T) {
// Switch to test mode so you don't get such noisy output
gin.SetMode(gin.TestMode)
// Setup your router, just like you did in your main function, and
// register your routes
r := gin.Default()
r.GET("/users", GetUsers)
// Create the mock request you'd like to test. Make sure the second argument
// here is the same as one of the routes you defined in the router setup
// block!
req, err := http.NewRequest(http.MethodGet, "/users", nil)
if err != nil {
t.Fatalf("Couldn't create request: %v\n", err)
}
// Create a response recorder so you can inspect the response
w := httptest.NewRecorder()
// Perform the request
r.ServeHTTP(w, req)
// Check to see if the response was what you expected
if w.Code != http.StatusOK {
t.Fatalf("Expected to get status %d but instead got %d\n", http.StatusOK, w.Code)
}
}
Although you could create a mock *gin.Context, it's probably easier to use the method above, since it'll execute and handle your request the same as it would an actual request.
If to reduce the question to "How to create mock for a function argument?" the answer is: use interfaces not concrete types.
type Context struct is a concrete type literal and Gin doesn't provide appropriate interface. But you can declare it by yourself. Since you are using only JSON method from Context you can declare extra-simple interface:
type JSONer interface {
JSON(code int, obj interface{})
}
And use JSONer type instead Context type in all your functions which expect Context as argument:
/* Note, you can't declare argument as a pointer to interface type,
but when you call it you can pass pointer to type which
implements the interface.*/
func GetUsers(c JSONer) {
repo := UserRepository{}
ctrl := UserController{}
ctrl.GetAll(c, repo)
}
func GetUser(c JSONer) {
repo := UserRepository{}
ctrl := UserController{}
ctrl.Get(c, repo)
}
func (ctrl UserController) GetAll(c JSONer, repository UserRepositoryIterface) {
c.JSON(200, repository.GetAll())
}
func (ctrl UserController) Get(c JSONer, repository UserRepositoryIterface) {
id := c.Param("id")
idConv, _ := strconv.Atoi(id)
c.JSON(200, repository.Get(idConv))
}
And now it is easy to test
type ContextMock struct {
JSONCalled bool
}
func (c *ContextMock) JSON(code int, obj interface{}){
c.JSONCalled = true
}
func TestControllerGetAll(t *testing.T) {
gin.SetMode(gin.TestMode)
c := &ContextMock{false}
c.Status(200)
repo := UserRepositoryMock{}
ctrl := UserController{}
ctrl.GetAll(c, repo)
if c.JSONCalled == false {
t.Fail()
}
}
Example simple as possible.
There is another question with a close sense