Introduction
Go is an excellent programming language for building HTTP servers, thanks to its net/http
package in the standard library, which makes it easy to attach HTTP handlers to any Go program. The standard library also includes packages that facilitate testing HTTP servers, making it just as effortless to test them as it is to build them.
Nowadays, test coverage is widely accepted as an essential and valuable part of software development. Developers invest time in testing their code to get quick feedback when making changes, and a good test suite becomes an invaluable component of the software project when combined with continuous integration and delivery methodologies.
Given the importance of a good test suite, what approach should developers using Go take when testing their HTTP servers? This article provides everything you need to know to test your Go HTTP servers thoroughly.
Http Server For Conversion of Roman Numerals
We will have a web server which gives the roman numeral of the given number. We will only have 1 endpoint.
- Show the roman numeral of the number
GET /roman
Example Request and Response
Request
1
| curl --location --request GET 'http://localhost:8080/roman?query=1'
|
Response
Code and Explanation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
)
var (
nums = []int{1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000}
symbols = []string{"I", "IV", "V", "IX", "X", "XL", "L", "XC", "C", "CD", "D", "CM", "M"}
)
func convertIntegerToRoman(input int) string {
var (
i = len(nums) - 1
result string
)
for input > 0 {
division := input / nums[i]
input = input % nums[i]
for division > 0 {
result += symbols[i]
division = division - 1
}
i = i - 1
}
return result
}
type romanHandler struct{}
func (h romanHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodGet {
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
return
}
input := r.URL.Query().Get("query")
inputInt, err := strconv.Atoi(input)
if err != nil {
http.Error(w, "invalid input", http.StatusBadRequest)
return
}
output := convertIntegerToRoman(inputInt)
response := map[string]interface{}{
"output": output,
}
if err := json.NewEncoder(w).Encode(&response); err != nil {
return
}
}
func main() {
mux := http.NewServeMux()
mux.Handle("/roman", romanHandler{})
log.Fatal(http.ListenAndServe(":8080", mux))
}
|
Points
The function convertIntegerToRoman
takes an integer and return the roman numeral conversion of the number. Please have a look on Convert Number Into Roman Numeral
We accept a single query parameter named query
in the URL which should have the number which will be converted.
The struct implements the http.Handler
interface by implementing the method of
ServeHTTP(ResponseWriter, *Request)
Testing Of the Server
The whole purpose of this blog was to learn how to test http servers in Go, so let’s find out.
As we mentioned in the beginning Go has all of the tools we need to both create net/http
and test net/http/httptest
. All of the tools are included in the net
module.
Let’s create a file named main_test.go
which has all of the tests for the HTTP Server.
Tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestRomanHandler(t *testing.T) {
tt := []struct {
name string
httpMethod string
query string
responseBody string
statusCode int
}{
{
name: "unsupported httpMethod",
httpMethod: http.MethodPost,
query: "1",
responseBody: "unsupported httpMethod",
statusCode: http.StatusMethodNotAllowed,
},
{
name: "invalid input",
httpMethod: http.MethodGet,
query: "asd",
responseBody: `invalid input`,
statusCode: http.StatusBadRequest,
},
{
name: "correct query param",
httpMethod: http.MethodGet,
query: "1",
responseBody: `{"output":"I"}`,
statusCode: http.StatusOK,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
path := fmt.Sprintf("/roman?query=%s", tc.query)
request := httptest.NewRequest(tc.httpMethod, path, nil)
responseRecorder := httptest.NewRecorder()
romanHandler{}.ServeHTTP(responseRecorder, request)
if responseRecorder.Code != tc.statusCode {
t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
}
if strings.TrimSpace(responseRecorder.Body.String()) != tc.responseBody {
t.Errorf("Want '%s', got '%s'", tc.responseBody, responseRecorder.Body)
}
})
}
}
|
To test the handler, we use the common table-driven approach and provide three cases:
- the http method is not correct
- http method is correct, but the query param is invalid
- both http method and query param is valid.
For each case, we run a subtest that creates a new request and a response recorder. We use the httptest.NewRequest
function to create an http.Request
struct, which represents an incoming request to the handler. This allows us to simulate a real request without relying on an actual HTTP server.
However, this function only handles the request half of the testing. To handle the response half, we use httptest.ResponseRecorder
, which records the mutations of the http.ResponseWriter
and enables us to make assertions on it later in the test.
By using this duo of httptest.ResponseRecorder
and http.Request
, we can successfully test any HTTP handler in Go. Running the test will produce the following output.
1
2
3
4
5
6
7
8
9
| === RUN TestRomanHandler
=== RUN TestRomanHandler/unsupported_method
=== RUN TestRomanHandler/invalid_input
=== RUN TestRomanHandler/correct_query_param
--- PASS: TestRomanHandler (0.00s)
--- PASS: TestRomanHandler/unsupported_method (0.00s)
--- PASS: TestRomanHandler/invalid_input (0.00s)
--- PASS: TestRomanHandler/correct_query_param (0.00s)
PASS
|
REFERENCES