I'm using gin-gonic for a server, and testify for testing and mocks, along with "testing" and "net/http/httptest"
The part of the interface that mocks the method:
func (m *MockInterface) Method(ctx context.Context, id string, deleted bool) ([]models.Entity, error) {
args := m.Called(ctx, id, deleted)
var entities []models.Entity
if args.Get(0) != nil {
entities = args.Get(0).([]models.Entity)
}
var err error
if args.Get(1) != nil {
err = args.Error(1)
}
return entities, err
}
Setting it up in a test - the server is setup outside of this t.Run, there are tests before this that run fine.
t.Run("TestName", func(t *testing.T) {
mockInterface := new(mocks.MockInterface)
mockInterface.On("Method", mock.AnythingOfType("*context.timerCtx"), id.String(), true).Return(mockResp, nil)
// a response writer to capture the response
rr := httptest.NewRecorder()
url := "SomeURLString"
// make the request to the Method handler
request, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err)
router.ServeHTTP(rr, request)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rr.Code)
mockInterface.AssertExpectations(t)
})
This is where it panics:
router.ServeHTTP(rr, request)
mock: Unexpected Method Call
-----------------------------
Method(*context.timerCtx,string,bool)
0: &context.timerCtx{cancelCtx:context.cancelCtx{Context:(*context.emptyCtx)...etc}}
1: "MatchingID"
2: true
The closest call I have is:
Method(mock.AnythingOfTypeArgument,string,bool)
0: "*context.timerCtx"
1: "MatchingID"
2: false
When I go into debug mode, mockInterface.Mock.ExpectedCalls[0].Arguments[2] is true, just as I set it. And then it panics and says it's not... while it is still true!
I've gone far enough into the stack to verify that the handler called method with the boolean as true, so it ought to pass. Instead it panics, and I'm not sure where to go from here to figure out why.
Does anyone know what is going on here? Is there some kind of odd interaction between gin and testify that I'm missing? Thank you.
Related
I have a simple Gin server with one of the routes called /metadata.
What the handler does is it reads a file from the system, say /etc/myapp/metadata.json and returns the JSON in the response.
But when the file is not found, handler is configured to return following error.
500: metadata.json does not exists or not readable
On my system, which has the metadata.json file, the test passes. Here is the test function I am using:
package handlers_test
import (
"net/http"
"net/http/httptest"
"testing"
"myapp/routes"
"github.com/stretchr/testify/assert"
)
func TestMetadataRoute(t *testing.T) {
router := routes.SetupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/metadata", nil)
router.ServeHTTP(w, req)
assert.NotNil(t, w.Body)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "field1")
assert.Contains(t, w.Body.String(), "field2")
assert.Contains(t, w.Body.String(), "field3")
assert.Contains(t, w.Body.String(), "field4")
}
But on CI environment, the test would fail because it won't find metadata.json. And would return the configured error.
What can be done?
I have this handler:
func GetMetadata(c *gin.Context) {
// read the info
content, err := ioutil.ReadFile("/etc/myapp/metadata.json")
if err != nil {
c.JSON(http.StatusInternalServerError,
gin.H{"error": "metadata.json does not exists or not readable"})
return
}
// deserialize to json
var metadata models.Metadata
err = json.Unmarshal(content, &metadata)
if err != nil {
c.JSON(http.StatusInternalServerError,
gin.H{"error": "unable to parse metadata.json"})
return
}
c.JSON(http.StatusOK, metadata)
}
What Volker is suggesting is to use a package-level unexported variable. You give it a fixed default value, corresponding to the path you need in production, and then simply overwrite that variable in your unit test.
handler code:
var metadataFilePath = "/etc/myapp/metadata.json"
func GetMetadata(c *gin.Context) {
// read the info
content, err := ioutil.ReadFile(metadataFilePath)
// ... rest of code
}
test code:
func TestMetadataRoute(t *testing.T) {
metadataFilePath = "testdata/metadata_test.json"
// ... rest of code
}
This is a super-simple solution. There are ways to improve on this, but all are variations of how to inject any variable in a Gin handler. For simple request-scoped configuration, what I usually do is to inject the variable into the Gin context. This requires slightly refactoring some of your code:
router setup code with middleware for production
func SetupRouter() {
r := gin.New()
r.GET("/metadata", MetadataPathMiddleware("/etc/myapp/metadata.json"), GetMetadata)
// ... rest of code
}
func MetadataPathMiddleware(path string) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("_mdpath", path)
}
}
handler code extracting the path from context:
func GetMetadata(c *gin.Context) {
metadataFilePath := c.GetString("_mdpath")
content, err := ioutil.ReadFile(metadataFilePath)
// ... rest of code
}
test code which you should refactor to test the handler only (more details: How to unit test a Go Gin handler function?):
func TestMetadataRoute(t *testing.T) {
// create Gin test context
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// inject test value into context
c.Set("_mdpath", "testdata/metadata_test.json")
// just test handler, the passed context holds the test value
GetMetadata(c)
// ... assert
}
Note: setting context values with string keys is somewhat discouraged, however the Gin context accepts only string keys.
I need to create a Pull Request comment using go-github, and my code works, but now I'd like to write tests for it (yes, I'm aware that tests should come first), so that I don't actually call the real GitHub service during test.
I've read 3 blogs on golang stubbing and mocking, but, being new to golang, I'm a bit lost, despite this discussion on go-github issues. For example, I wrote the following function:
// this is my function
func GetClient(token string, url string) (*github.Client, context.Context, error) {
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
tc := oauth2.NewClient(ctx, ts)
client, err := github.NewEnterpriseClient(url, url, tc)
if err != nil {
fmt.Printf("error creating github client: %q", err)
return nil, nil, err
}
return client, ctx, nil
}
How could I stub that?
Similarly, I have this:
func GetPRComments(ctx context.Context, client *github.Client) ([]*github.IssueComment, *github.Response, error) {
opts := &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
Page: 1,
PerPage: 30,
},
}
githubPrNumber, err := strconv.Atoi(os.Getenv("GITHUB_PR_NUMBER"))
if err != nil || githubPrNumber == 0 {
panic("error: GITHUB_PR_NUMBER is not numeric or empty")
}
// use Issues API for PR comments since GitHub docs say "This may seem counterintuitive... but a...Pull Request is just an Issue with code"
comments, response, err := client.Issues.ListComments(
ctx,
os.Getenv("GITHUB_OWNER"),
os.Getenv("GITHUB_REPO"),
githubPrNumber,
opts)
if err != nil {
return nil, nil, err
}
return comments, response, nil
}
How should I stub that?
My thought was to perhaps use dependency injection by creating my own structs first, but I'm not sure how, so currently I have this:
func TestGetClient(t *testing.T) {
client, ctx, err := GetClient(os.Getenv("GITHUB_TOKEN"), "https://example.com/api/v3/")
c, r, err := GetPRComments(ctx, client)
...
}
I would start with an interface:
type ClientProvider interface {
GetClient(token string, url string) (*github.Client, context.Context, error)
}
When testing a unit that needs to call GetClient make sure you depend on your ClientProvider interface:
func YourFunctionThatNeedsAClient(clientProvider ClientProvider) error {
// build you token and url
// get a github client
client, ctx, err := clientProvider.GetClient(token, url)
// do stuff with the client
return nil
}
Now in your test, you can construct a stub like this:
// A mock/stub client provider, set the client func in your test to mock the behavior
type MockClientProvider struct {
GetClientFunc func(string, string) (*github.Client, context.Context, error)
}
// This will establish for the compiler that MockClientProvider can be used as the interface you created
func (provider *MockClientProvider) GetClient(token string, url string) (*github.Client, context.Context, error) {
return provider.GetClientFunc(token, url)
}
// Your unit test
func TestYourFunctionThatNeedsAClient(t *testing.T) {
mockGetClientFunc := func(token string, url string) (*github.Client, context.Context, error) {
// do your setup here
return nil, nil, nil // return something better than this
}
mockClientProvider := &MockClientProvider{GetClientFunc: mockGetClientFunc}
// Run your test
err := YourFunctionThatNeedsAClient(mockClientProvider)
// Assert your result
}
These ideas aren't my own, I borrowed them from those who came before me; Mat Ryer suggested this (and other ideas) in a great video about "idiomatic golang".
If you want to stub the github client itself, a similar approach can be used, if github.Client is a struct, you can shadow it with an interface. If it is already an interface, the above approach works directly.
I am coding unit tests in my Go API with Gin Gonic.
Here is my code.
func getKeys(c *gin.Context) {
var meters []models.Meter
metadataOperation, err := metadata.GetOperation("AC123456")
if err != nil {
sendInternalError(err, c)
return
}
meter, err := metadata.GetMeter("12345")
// Other instructions
// ...
// operation = ...
c.JSON(http.StatusOK, operation)
}
Here is GetOperation method:
func GetOperation(operationID string) (Operation, error) {
var operation Operation
var url = metadataAPIURL + "/v2/operations/" + operationID
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return Operation{}, err
}
req.SetBasicAuth(metadataAPIUser, metadataAPIPassword)
res, err := client.Do(req)
if err != nil {
return Operation{}, err
}
if res.StatusCode != 200 {
return Operation{}, errors.New(res.Status)
}
err = json.NewDecoder(res.Body).Decode(&operation)
if err != nil {
return Operation{}, err
}
return operation, nil
}
Thing is metadata.GetOperation("AC123456") will make a GET request to an external service.
As I understand unit testing, I can't have any external dependencies.
In my case, test is passing, but it is making a GET request to my production server which is not the wanted result.
If I want to use mocks, I should have an interface, and switch between dependency, and mock.
It should be great to test GetOperation method, but for getKeys method, it seems unclear to me how should I do it.
How should I deal with this situation? Can anyone give me an example / tuto about this case.
First, refactor your GetOperation method to accept the URL as parameter.
func GetOperation(url, operationID string) (Operation, error)...
Then, use net/http/httptest and create a test server:
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
res.Write(expectedData)
}))
defer func() { testServer.Close() }()
Finally, pass the test server URL as parameter to GetOperation:
GetOperation(testServer.URL, 'some-operation')
Validate that the function calls the url correctly and retrieves the expectedData you've passed into the test server.
So there are two common ways to do that in unit tests, that I know.
First is to mock the request (e.g. Create Requester interface or something like that to wrap real GET request) and then replace it with a mock object in unit-test. It called dependency injection.
The second way is to run the test server using net/http/httptest and replace metadataAPIURL to localhost URL. See the example here.
My project is split into three main components: controllers, services, and models. When a route is queried via the URI, the controllers are called, which then call the services to interact with the models, which then interact with the database via gorm.
I am trying to write unit tests for the controllers, but I'm having a hard time understanding how to properly mock the services layer while mocking the gin layer. I can get a mocked gin context, but I'm not able to mock the service layer within my controller method. Below is my code:
resourceController.go
package controllers
import (
"MyApi/models"
"MyApi/services"
"github.com/gin-gonic/gin"
"net/http"
)
func GetResourceById(c *gin.Context) {
id := c.Param("id")
resource, err := services.GetResourceById(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": err})
return
} else if resource.ID == 0 {
c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "Resource with id:"+id+" does not exist"})
return
}
c.JSON(http.StatusOK, gin.H{
"id": resource.ID,
"data1": resource.Data1,
"data2": resource.Data2,
})
}
I want to test that the c.JSON is returning with the proper http status and other data. I need to mock the id variable, err variable, and c.JSON function, but when I try to set the c.JSON function in the test to my new function, I get an error saying Cannot assign to c.JSON.
Below is my attempt at writing a test:
resourceController_test.go
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"net/http/httptest"
"testing"
)
func TestGetResourceById(t *testing.T) {
var status int
var body interface{}
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.JSON = func(stat int, object interface{}) {
status = stat
body = object
}
GetResourceById(c)
assert.Equal(t, 4, 4)
}
How do I properly write a unit test to test whether the c.JSON is returning the proper values?
You cannot modify a method of a type in Go. It is defined and immuatable by the package that defines the type at compile time. This is a design decision by Go. Simply don't do it.
You have already use httptest.NewRecorder() as a mock of gin.Context.ResponseWriter, which will records what is written to the response, including the c.JSON call. However, you need to keep a reference of the httptest.ReponseRecorder and then check it later. Note that you only have a marshalled JSON, so you need to unmarshal it to check content (as both Go map and JSON objects's order does not matter, checking marshalled string's equality is error-prone).
For example,
func TestGetResourceById(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
GetResourceById(c)
assert.Equal(t, 200, w.Code) // or what value you need it to be
var got gin.H
err := json.Unmarshal(w.Body.Bytes(), &got)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, want, got) // want is a gin.H that contains the wanted map.
}
Based on the testing section, you can do something like:
func TestGetResourceById(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/GetResourceById", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "your expected output", w.Body.String())
}
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?