Golang: Testing function in a simple web server project

icelandcheng
Nerd For Tech
Published in
7 min readNov 27, 2022

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”.

From testing online document

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.

From net/http/httptest online document

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 httptestuseful 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

Learn Go with test

Go official online document

使用Golang打造web 應用程式

Build Web Application with Golang

--

--

icelandcheng
Nerd For Tech

Programming Skill learner and Sharer | Ruby on Rails | Golang | Vue.js | Web Map API