I am running UT on some rest calls by creating a http test server in the Go language. My code is as follows.
type student struct{
FirstName string
LastName string
}
func testGetStudentName() {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response = new(student)
response.FirstName = "Some"
response.LastName = "Name"
b, err := json.Marshal(response)
if err == nil {
fmt.Fprintln(w, string(b[:]))
}
}))
defer ts.Close()
student1 := base.getStudent("123")
log.Print("testServerUrl",testServer.URL) //prints out http://127.0.0.1:49931 ( port changes every time this is run)
ts.URL = "http://127.0.0.1:8099" //this assignment does not quite change the URL of the created test server.
}
In the file being tested,
var baseURL = "http://originalUrl.com"
var mockUrl = "http://127.0.0.1:49855"
func Init(mockServer bool){
if mockServer {
baseURL = mockUrl
}
}
func getStudent(id String){
url := baseUrl + "/student/" + id
req, err := http.NewRequest("GET", url, nil)
}
This init is called from my test.
This creates a new test server and runs the calls on a random port. Is it possible for me to run this server on a port I specify?
Most applications use the port assigned in NewServer or NewUnstartedServer because this port will not conflict with a port in use on the machine. Instead of assigning a port, they set the base URL for the service to the test server's URL.
If you do want to set the listening port, do the following:
// create a listener with the desired port.
l, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
ts := httptest.NewUnstartedServer(handler)
// NewUnstartedServer creates a listener. Close that listener and replace
// with the one we created.
ts.Listener.Close()
ts.Listener = l
// Start the server.
ts.Start()
// Stop the server on return from the function.
defer ts.Close()
// Add your test code here.
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 write this go code for login. Now i want to unit test my code. This code is depends on controller to service layer then service to repository layer. I want to use gomock tool for mocking, if any other please suggest me. I'm using echo framework.
Here
serializers.LoginReq =
{
Email string,
Phone string,
Admin bool
}
type auth struct {
authSvc svc.IAuth
userSvc svc.IUsers
}
func NewAuthController(grp interface {}, authSvc svc.IAuth, userSvc svc.IUsers) {
ac: = & auth {
authSvc: authSvc,
userSvc: userSvc,
}
g: = grp.( * echo.Group)
g.POST("/v1/login", ac.Login)
}
func(ctr * auth) Login(c echo.Context) error {
var cred * serializers.LoginReq
var resp * serializers.LoginResp
var err error
if err = c.Bind( & cred) err != nil {
return c.JSON(err.Status, err)
}
if resp, err = ctr.authSvc.Login(cred); err != nil {
return c.JSON(err.Status, err)
}
return c.JSON(http.StatusOK, resp)
}
Use dependency injection. Dependency injection is a design pattern that decouples dependencies between two or more layers of software.
How it works
Pass a dependency to the Login function. In Go, the dependency is often an interface type. Interfaces express generalizations or abstractions about the behaviors of other types. A type satisfies an interface if it has all the methods in the interface. With an interface, you can replace a real object with a fake one (a mock) in your tests. This works without Go's type system complaining as long as a concrete type satisfies the interface.
type Auther interface {
Login(cred *serializers.LoginReq) (*serializers.LoginResp, error)
}
Go Interfaces are satisfied implicitly.
// auth service must implement the Auther interface
type auth struct {
authSvc Auther
}
// route handler
func(ctr *auth) Login(c echo.Context) error {
var cred * serializers.LoginReq
var resp * serializers.LoginResp
var err error
if err = c.Bind( & cred) err != nil {
return c.JSON(err.Status, err)
}
// the function signature of the service-level Login method must match the interface
if resp, err = ctr.authSvc.Login(cred); err != nil {
return c.JSON(err.Status, err)
}
return c.JSON(http.StatusOK, resp)
}
I like using testify/mock library. Create a Mock.
type MockAuth struct {
mock.Mock
}
func (m *MockAuth) Login(cred *serializers.LoginReq) (*serializers.LoginResp, error) {
args := m.Called(cred)
return args.Get(0).(*serializers.LoginResp), args.Error(1)
}
That's it. Just create a test.
func TestLogin (t *testing.T) {
// setup mocks
cred := &serializers.LoginReq{}
mockReturn := &serializers.LoginResp{}
mockAuth := &MockAuth{}
// setup expectation
mockAuth.On("Login", cred).Return(mockReturn, nil)
// setup server
mux := http.NewServeMux()
mux.HandleFunc("/v1/login", func(w http.ResponseWriter, r *http.Request) {
ec := echo.Context{}
ctr: = &auth {
authSvc: mockAuth
}
ctr.Login(ec)
})
// make request
writer := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodPost, "/v1/login", "password")
mux.ServeHTTP(writer, request)
// make assertions
mockAuth.AssertExpectations(t)
}
The code above is not 100% correct. I don't use echo myself, however it should get you close. Hope this helps.
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.
I am trying to write unit tests for my rest endpoints using Go-Chi as my mux.
Previously I was using gorilla/mux but moved to Chi because it is easier to maintain as my application grows.
With Gorilla/mux, I was able to use "ServeHTTP" to send a test request but using go-chi/chi it does not seem to do the same thing.
var writer *httptest.ResponseRecorder
var r = chi.Mux{}
func TestMain(m *testing.M) {
setUp()
code := m.Run()
os.Exit(code)
}
func setUp() {
d, _ := database.ConnectToDB(database.TESTDBNAME)
writer = httptest.NewRecorder()
r := chi.NewMux()
r.Route("/companies", func(r chi.Router) {
r.Get("/", GetCompanies(d))
r.Get("/{id}", GetCompany(d))
r.Post("/", PostCompany(d))
r.Put("/{id}", PutCompany(d))
r.Delete("/{id}", DeleteCompany(d))
})
}
func TestPostCompany(t *testing.T) {
tables := []struct {
company model.Company
result int
}{
{model.Company{Name:"Test"}, 200},
}
for _, table := range tables {
company, err := json.Marshal(table.company)
if err != nil {
t.Errorf("JSON Error")
}
companyJson := strings.NewReader(string(company))
request, err := http.NewRequest("POST", "/companies", companyJson)
if err != nil {
t.Error(err)
}
r.ServeHTTP(writer, request)
if writer.Code != table.result {
t.Error(writer.Body)
}
}
}
Right now the test is showing a 404 error but I would like it to give a 200 error. This request works fine while running my application and testing manually.
I believe the issue has something to do with "ServeHTTP". Maybe it works differently with chi. Does anyone know how to get this test to run successfully?
I am stuck over testing with mocking, Here is my route for handler:
r.Handle("/users/{userID}", negroni.New(
negroni.HandlerFunc(validateTokenMiddleware),
negroni.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
getUserDetailsHandler(w, r, db)
})),
)).Methods("GET")
And here is my handler:
func getUserDetailsHandler(w http.ResponseWriter, r *http.Request, db *sql.DB) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
//Create UserDetailsView instance
var userview UserDetailsView
//Get varibale from mux
vars := mux.Vars(r)
//UserID fetches userId from vars
userID := vars["userID"]
//Get user Information by wpUsersID
wuis := store.NewWpUserInformationStore(db)
userInformation, _:= wuis.GetByID(uID)
json.NewEncoder(w).Encode(userview);
//Print result
w.WriteHeader(http.StatusOK)
}
And i mock the function which is in store package named as GetByID which is looks like this :
type wpUserInfoMockStore struct {
mock.Mock
}
func (m *wpUserInfoMockStore) GetByID(user *WpUserInformation) error {
rets := m.Called(user)
return rets.Error(0)
}
//InitMockStore store
func InitMockStore() *wpUserInfoMockStore {
s := new(wpUserInfoMockStore)
//store = s
return s
}
And i write test case for handler but i got an error cannot convert getUserDetailsHandler (type func(http.ResponseWriter, *http.Request, *sql.DB)) to type http.HandlerFunc but i can not find why is it happened, here i'm using reference for this https://github.com/sohamkamani/blog_example__go_web_db and here is my test case code:
func TestGetUserDetailsTes(t *testing.T) {
// Initialize the mock store
mockStore := store.InitMockStore()
mockStore.On("GetByID").Return([]*store.WpUserInformation{{
21,
sql.NullString{String: "john"},
sql.NullString{String: "Sorensen"},
0}}, nil).Once()
req, err := http.NewRequest("GET", "", nil)
//if requests gives error
if err != nil {
panic(err.Error())
}
//parameters for generateTestUserJWT are set
testUser.ID = "22"
testUser.UserName = "johns"
testUser.Depot = "NYC"
//JWT generated
refToken, err := generateTestJWT(testUser, false)
//handling error while generating token
if err != nil {
panic(err.Error())
}
//token returned is concatenated with Bearer string
newToken = "Bearer " + refToken
//request authorization header is set
req.Header.Set("Authorization", newToken)
req.Header.Set("Latitude", "123.12")
req.Header.Set("Longitude", "456.45")
//response is set
w := httptest.NewRecorder()
hf := http.HandlerFunc(getUserDetailsHandler)
hf.ServeHTTP(w, req)
//if response code is not statusOK then test fails
if w.Code != http.StatusOK {
t.Errorf("/users/{userID} GET request failed, got: %d, want: %d.", w.Code, http.StatusOK)
}
}
As you see i test handler without url like req, err := http.NewRequest("GET", "", nil) but when i used link inside then i can not able to use mock functions, here what am i missing/fault please help me out.
Thank you.
Use a middleware handler for generating the function. Pass a handler in your main function which will call your middleware returning http.handler. That way you can pass db object to your main data and which will call the middle ware returning handler.
func getUserDetailsHandler(w http.ResponseWriter, r *http.Request, db *sql.DB) http.HandlerFunc{
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
//Create UserDetailsView instance
var userview UserDetailsView
//Get varibale from mux
vars := mux.Vars(r)
//UserID fetches userId from vars
userID := vars["userID"]
//Get user Information by wpUsersID
wuis := store.NewWpUserInformationStore(db)
userInformation, _:= wuis.GetByID(uID)
json.NewEncoder(w).Encode(userview);
//Print result
w.WriteHeader(http.StatusOK)
}
}