One advantage of the Go language is its standard library, which contains many useful features to develop modern applications, such as HTTP server and client, JSON parser, and tests. It is exactly this last point that I will talk about in this post.
With the standard library it is possible to write tests for your API, as in the following example.
API code
In ourmain.go
file, we will create a simple API:
packagemainimport("encoding/json""log""net/http""os""strconv""time""github.com/codegangsta/negroni""github.com/gorilla/context""github.com/gorilla/mux")//Bookmark datatypeBookmarkstruct{IDint`json:"id"`Linkstring`json:"link"`}funcmain(){//routerr:=mux.NewRouter()//midllewaresn:=negroni.New(negroni.NewLogger(),)//routesr.Handle("/v1/bookmark",n.With(negroni.Wrap(bookmarkIndex()),)).Methods("GET","OPTIONS").Name("bookmarkIndex")r.Handle("/v1/bookmark/{id}",n.With(negroni.Wrap(bookmarkFind()),)).Methods("GET","OPTIONS").Name("bookmarkFind")http.Handle("/",r)//serverlogger:=log.New(os.Stderr,"logger: ",log.Lshortfile)srv:=&http.Server{ReadTimeout:5*time.Second,WriteTimeout:10*time.Second,Addr:":8080",Handler:context.ClearHandler(http.DefaultServeMux),ErrorLog:logger,}//start servererr:=srv.ListenAndServe()iferr!=nil{log.Fatal(err.Error())}}funcbookmarkIndex()http.Handler{returnhttp.HandlerFunc(func(whttp.ResponseWriter,r*http.Request){data:=[]*Bookmark{{ID:1,Link:"http://google.com",},{ID:2,Link:"https://apitest.dev",},}w.Header().Set("Content-Type","application/json")iferr:=json.NewEncoder(w).Encode(data);err!=nil{w.WriteHeader(http.StatusInternalServerError)w.Write([]byte("Error reading bookmarks"))}})}funcbookmarkFind()http.Handler{returnhttp.HandlerFunc(func(whttp.ResponseWriter,r*http.Request){vars:=mux.Vars(r)id,err:=strconv.Atoi(vars["id"])iferr!=nil{w.WriteHeader(http.StatusInternalServerError)w.Write([]byte("Error reading parameters"))return}data:=[]*Bookmark{{ID:2,Link:"https://apitest.dev",},}ifid!=data[0].ID{w.WriteHeader(http.StatusNotFound)w.Write([]byte("Not found"))return}w.Header().Set("Content-Type","application/json")iferr:=json.NewEncoder(w).Encode(data[0]);err!=nil{w.WriteHeader(http.StatusInternalServerError)w.Write([]byte("Error reading bookmark"))}})}
Compiling
First, we need to start our project as a module, and install the external dependencies, such asnegroni andgorilla. For this we execute the command:
go mod init github.com/eminetto/post-apitestgo: creating new go.mod: module github.com/eminetto/post-apitest
Now, running the build command the compilation process will install the dependencies:
go buildgo: finding module for package github.com/gorilla/contextgo: finding module for package github.com/gorilla/muxgo: finding module for package github.com/codegangsta/negronigo: found github.com/codegangsta/negroni in github.com/codegangsta/negroni v1.0.0go: found github.com/gorilla/context in github.com/gorilla/context v1.1.1go: found github.com/gorilla/mux in github.com/gorilla/mux v1.7.4
Testing with the standard library
We will now create the tests for this API. Ourmain_test.go
file looks like this:
packagemainimport("net/http""net/http/httptest""testing""github.com/gorilla/mux")funcTest_bookmarkIndex(t*testing.T){r:=mux.NewRouter()r.Handle("/v1/bookmark",bookmarkIndex())ts:=httptest.NewServer(r)deferts.Close()res,err:=http.Get(ts.URL+"/v1/bookmark")iferr!=nil{t.Errorf("Expected nil, received %s",err.Error())}ifres.StatusCode!=http.StatusOK{t.Errorf("Expected %d, received %d",http.StatusOK,res.StatusCode)}}funcTest_bookmarkFind(t*testing.T){r:=mux.NewRouter()r.Handle("/v1/bookmark/{id}",bookmarkFind())ts:=httptest.NewServer(r)deferts.Close()t.Run("not found",func(t*testing.T){res,err:=http.Get(ts.URL+"/v1/bookmark/1")iferr!=nil{t.Errorf("Expected nil, received %s",err.Error())}ifres.StatusCode!=http.StatusNotFound{t.Errorf("Expected %d, received %d",http.StatusNotFound,res.StatusCode)}})t.Run("found",func(t*testing.T){res,err:=http.Get(ts.URL+"/v1/bookmark/2")iferr!=nil{t.Errorf("Expected nil, received %s",err.Error())}ifres.StatusCode!=http.StatusOK{t.Errorf("Expected %d, received %d",http.StatusOK,res.StatusCode)}})}
Running the tests, we see that everything is passing successfully:
go test -v=== RUN Test_bookmarkIndex--- PASS: Test_bookmarkIndex (0.00s)=== RUN Test_bookmarkFind=== RUN Test_bookmarkFind/not_found=== RUN Test_bookmarkFind/found--- PASS: Test_bookmarkFind (0.00s) --- PASS: Test_bookmarkFind/not_found (0.00s) --- PASS: Test_bookmarkFind/found (0.00s)PASSok github.com/eminetto/post-apitest 0.371s
That way, we can test our API using only the standard library. But the tests code aren’t so readable, especially when we are testing a large API, with severalendpoints.
Using apitest
To improve our test code, we can use some third-party libraries, such asapitest, which simplifies the process.
Let’s start by installing the new packages. At the terminal, we execute:
go get github.com/steinfletcher/apitestgo: github.com/steinfletcher/apitest upgrade => v1.4.5
and
go get github.com/steinfletcher/apitest-jsonpathgo: github.com/steinfletcher/apitest-jsonpath upgrade => v1.5.0
Now let’s alter themain_test.go
file:
packagemainimport("net/http""net/http/httptest""testing""github.com/gorilla/mux""github.com/steinfletcher/apitest"jsonpath"github.com/steinfletcher/apitest-jsonpath")funcTest_bookmarkIndex(t*testing.T){r:=mux.NewRouter()r.Handle("/v1/bookmark",bookmarkIndex())ts:=httptest.NewServer(r)deferts.Close()apitest.New().Handler(r).Get("/v1/bookmark").Expect(t).Status(http.StatusOK).End()}funcTest_bookmarkFind(t*testing.T){r:=mux.NewRouter()r.Handle("/v1/bookmark/{id}",bookmarkFind())ts:=httptest.NewServer(r)deferts.Close()t.Run("not found",func(t*testing.T){apitest.New().Handler(r).Get("/v1/bookmark/1").Expect(t).Status(http.StatusNotFound).End()})t.Run("found",func(t*testing.T){apitest.New().Handler(r).Get("/v1/bookmark/2").Expect(t).Assert(jsonpath.Equal(`$.link`,"https://apitest.dev")).Status(http.StatusOK).End()})}
The tests became much more readable, and we gained the functionality to test the resulting JSON. A note: it is also possible to test the resulting JSON using only the standard library, but we need to add a few more lines in the test.
In thedocumentation you can see how powerful the library is, allowing advanced settings forheaders,cookies,debug andmocks. It is worth taking the time to study the options and see the examples provided.
Reports
An interesting feature that I would like to show in this post is reports generation. We need to make a slight change in the code, adding the lineReport(apitest.SequenceDiagram())
in the tests, as in the example:
apitest.New().Report(apitest.SequenceDiagram()).Handler(r).Get("/v1/bookmark").Expect(t).Status(http.StatusOK).End()
And when we run the tests again, we have the following result:
go test -v=== RUN Test_bookmarkIndexCreated sequence diagram (3157381659_2166136261.html): /Users/eminetto/Projects/post-apitest/.sequence/3157381659_2166136261.html--- PASS: Test_bookmarkIndex (0.00s)=== RUN Test_bookmarkFind=== RUN Test_bookmarkFind/not_foundCreated sequence diagram (1543772695_2166136261.html): /Users/eminetto/Projects/post-apitest/.sequence/1543772695_2166136261.html=== RUN Test_bookmarkFind/foundCreated sequence diagram (1560550314_2166136261.html): /Users/eminetto/Projects/post-apitest/.sequence/1560550314_2166136261.html--- PASS: Test_bookmarkFind (0.00s) --- PASS: Test_bookmarkFind/not_found (0.00s) --- PASS: Test_bookmarkFind/found (0.00s)PASSok github.com/eminetto/post-apitest 0.296s
These are the generated reports:
Is it worth using?
This is a question that has no single answer. Using only the standard library, the project gains speed in the execution of tests, besides not depending on third-party libraries, which can be a problem in some teams.
With a library like apitest you gain in productivity and ease of maintenance, but you lose in speed of execution. A note on speed: I ran just a few simple tests and benchmarks, so I can’t say for sure how big is the difference compared to the standard library, but an overhead is visible.
Each team can make its benchmarks and make that decision, but most of the time I believe that the team’s productivity will gain several points in this choice.
Top comments(3)
For further actions, you may consider blocking this person and/orreporting abuse