Golang: Testing function in a simple web server project
Testing is important when building a software project. In Golang, there are some packages really useful when doing the unit tests for the functions in our project. In this article, I will show how to use those packages to test a function in a web server project. the project structure is like below. We will test Greeting
function as an example. How to use Golang to build a web server could see in my previous article.
The code in main.go
is like below.
// main.go
package main
import (
"log"
"net"
"net/http"
"my-project/greeting"
)
func main() {
http.HandleFunc("/greeting", greeting.Greeting)
log.Println("Starting server....")
listener, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
http.Serve(listener, nil)
}
And greeting.go
is like below.
// greeting.go
package greeting
import (
"fmt"
"net/http"
)
func Greeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
}
1. Introduction
Golang provide package testing
for us to do unit tests and also provide package net/http/httptest
which could use for recording HTTP(or HTTPS) responses when doing the unit test. Following are the introductions of these two packages and the functions which are useful for doing unit tests:
Package testing
provides support for automated testing of Go packages.
To write a new test suite, create a file whose name ends _test.go that contains the TestXxx functions as described here. Put the file in the same package as the one being tested. The file will be excluded from regular package builds but will be included when the “go test” command is run. For more detail, run “go help test” and “go help testflag”.
A sample test function would like below.
func TestAbs(t *testing.T) {
got := Abs(-1)
want := 1
if got != want {
t.Errorf("Abs(-1) = %d; want %d", got, want)
}
}
Package httptest
provides utilities for HTTP testing. It provides NewRequest
function to simulate an incoming server Request, suitable for passing to an http.Handler for testing. method
, target
, and body
should pass to NewRequest
function. The method
is HTTP methods like GET
. The target
could be a path or an absolute URL
, and the body
could be nil.
The provided body may be nil. If the body is of type *bytes.Reader, *strings.Reader, or *bytes.Buffer, the Request.ContentLength is set.
func NewRequest(method, target string, body io.Reader) *http.Request
The sample code for NewRequest
function.
request := httptest.NewRequest("GET", "http://example.com/greeting", nil)
Package httptest
provides a type that calls ResponseRecorder
. It is an implementation of http.ResponseWriter
that records its mutations for later inspection in tests, which means that it could use for recording the content of the response for HTTP(or HTTPS) requests in tests.
type ResponseRecorder struct {
// Code is the HTTP response code set by WriteHeader.
Code int
// HeaderMap contains the headers explicitly set by the Handler.
// It is an internal detail.
HeaderMap http.Header
// Body is the buffer to which the Handler's Write calls are sent.
// If nil, the Writes are silently discarded.
Body *bytes.Buffer
// Flushed is whether the Handler called Flush.
Flushed bool
// contains filtered or unexported fields
}
Package httptest
also provides a function call NewRecorder
that could return an initialized ResponseRecorder
for recording the simulation of an HTTP(or HTTPS) response.
func NewRecorder() *ResponseRecorder
There is another function in package httptest
useful in the test. It is Result
function.
func (rw *ResponseRecorder) Header() http.Header
In the sample code below. We could see after running the handler
function, the w
could call Result
function which will return the response generated by the handler. The returned Response will have at least its StatusCode
, Header
, Body
. Result
must only be called after the handler has finished running.
handler := func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
}
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body := w.Body
fmt.Println(resp.StatusCode)
fmt.Println(resp.Header)
fmt.Println(body)
------------------------------------------------------------------------------
Output:
200
map[Content-Type:[text/plain; charset=utf-8]]
Hello World
2. Start testing
For testing, first, we need to add a file whose name ends _test.go
that contains the TestXxx function because we will use the package testing
and that’s the rule it works, so there will be a main_test.go
file in the same folder as main.go
. The structure of our project will be like the below picture.
We are going to test Greeting
function first, so the function name in main_test.go
is TestGreeting
. The content of main_test.go
would like below.
// main_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
// we need to call Greeting function, so need to import greeting
"my-project/greeting"
)
func TestGreeting(t *testing.T) {
// simulate an incoming server Request
request, _ := http.NewRequest("GET", "/greeting", nil)
// record the simulation of HTTP response
response := httptest.NewRecorder()
// run the function we want to test
greeting.Greeting(response, request)
// check if the result is what we expect
got := response.Body.String()
want := "Hello World"
if got != want {
// if the result is not correct print error
t.Errorf("got %v, want %v", got, want)
}
}
To run the test, we need to run go test
command in the root path of our project in the terminal. If you using VScode
as the editor, after installing Golang extension, we could run the test by clicking a button right next to the line of testing function.
After clicking the run button, we could see the result in VScode
terminal. If the testing pass, it will show a green check right next to the testing function, and also it will print the log and show PASS
in the terminal.
If the test failed, it will show the error we got.
If we want to add some description in the test log, like more detail about what we are testing, we could use t.Run
to wrap our testing function like below.
// main_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
"my-project/greeting"
)
func TestGreeting(t *testing.T) {
// use t.Run to wrap your test
t.Run("test greeting ok", func(t *testing.T) {
request, _ := http.NewRequest("GET", "/greeting", nil)
response := httptest.NewRecorder()
greeting.Greeting(response, request)
got := response.Body.String()
want := "Hello World"
if got != want {
t.Errorf("got %v, want %v", got, want)
}
})
}
And after running the test, we can see the description we put in t.Run
shows in the log.
3. Refactoring code
We now write our test in main_test.go
, but it is not a good place to run the test. When people see the file, it is hard to know what it is going to test, so we should create a test file with a name that includes the function we want to test and place it in the folder where the function file is. It will be easier for other people associate to with those functions we are testing.
Like we have Greeting
function in greeting.go
which in greeting
folder, we should place the test file in the same folder, and name it as greeting_test.go
so when everyone sees the file could know it is testing for the function in greeting.go
. The structure of the project will become like below.
And greeting_test.go
is like below. We should move the test function in greeting_test.go
.
// greeting_test.go
package greeting
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGreeting(t *testing.T) {
t.Run("test greeting ok", func(t *testing.T) {
request, _ := http.NewRequest("GET", "/greeting", nil)
response := httptest.NewRecorder()
Greeting(response, request)
got := response.Body.String()
want := "Hello World"
if got != want {
t.Errorf("got %v, want %v", got, want)
}
})
}
Clicking the run test button, the test function should still run successfully.
Now we know how to do the unit test, it’s also important to know what is the test coverage of our code. We could run go test -cover ./…
in the root of our project in the terminal to see the result.
In the test result, we could see in greeting
folder, the test coverage is 100%, because there is only Greeting
function in the greeting
folder needs to be tested and we already have a test case for it in greeting_test.go
.
Reference
Build Web Application with Golang