A Simple Go JSON API Server with Tests
I'm trying to make a small bookmarks server. I'm using code similar to this to create a server, and test 2 GET APIs. I'm incorporating the following ideas from Advanced Testing in Go:
- Subtests/Table driven tests
- Golden files
- Test fixtures
I'm not sure this code is exactly what I want, but I do think its' a good starting place:
main.go
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)
// NormalizedColumn is the json structure
type NormalizedColumn struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// NewServer returns a server to serve from
func NewServer() (*Server, error) {
data1 := []NormalizedColumn{
{1, "one"},
{2, "two"},
}
data2 := []NormalizedColumn{
{3, "three"},
{4, "four"},
}
return &Server{data1, data2}, nil
}
// Server that has data to use
type Server struct {
data1 []NormalizedColumn
data2 []NormalizedColumn
}
func (s *Server) getData1(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", " ") // NOTE: pretty-printing might not be good on an API
e.Encode(s.data1)
}
func (s *Server) getData2(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", " ") // NOTE: pretty-printing might not be good on an API
e.Encode(s.data2)
}
func main() {
s, err := NewServer()
if err != nil {
log.Fatal(err)
}
router := httprouter.New()
router.GET("/getData1", s.getData1)
router.GET("/getData2", s.getData2)
log.Fatal(http.ListenAndServe(":8080", router))
}
main_test.go
package main
import (
"bytes"
"flag"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/julienschmidt/httprouter"
)
// https://medium.com/@povilasve/go-advanced-tips-tricks-a872503ac859
var update = flag.Bool("update", false, "update .golden files")
// TestGetDatas tests all GET REST APIs
func TestGetDatas(t *testing.T) {
// Create Server
s, err := NewServer()
if err != nil {
t.Fatalf("Server Error: %+v\n", err)
}
// Create cases
cases := []struct {
testName string
serverMethod httprouter.Handle
httpMethod string
urlPath string
}{
{"getData1", s.getData1, "GET", "/getData1"},
{"getData2", s.getData2, "GET", "/getData2"},
}
for _, tt := range cases {
t.Run(tt.testName, func(t *testing.T) {
// Record request to server
req := httptest.NewRequest(tt.httpMethod, tt.urlPath, nil)
w := httptest.NewRecorder()
tt.serverMethod(w, req, nil)
resp := w.Result()
body, err := ioutil.ReadAll(resp.Body)
// Test common things
if resp.StatusCode != http.StatusOK {
t.Errorf("Status Code: %#v != http.StatusOK: %#v\n", resp.StatusCode, http.StatusOK)
}
contentType := resp.Header.Get("Content-Type")
if contentType != "application/json" {
t.Errorf("Content-Type: %#v != 'application/json'", contentType)
}
// Test body from golden file
testDir := "testdata"
golden := filepath.Join(testDir, tt.testName+".golden.json")
if *update {
err := os.MkdirAll(testDir, 0755)
if err != nil {
t.Fatalf("Mkdir Failure: %#v\n", err)
}
ioutil.WriteFile(golden, body, 0644)
} else {
if _, err := os.Stat(testDir); os.IsNotExist(err) {
t.Fatalf("Run `go test -update` to create the test data")
}
}
expected, err := ioutil.ReadFile(golden)
if err != nil {
if err != nil {
t.Fatalf("ReadFile Failure: %#v\n", err)
}
}
if !bytes.Equal(body, expected) {
t.Logf("Actual:\n%#v", body)
t.Logf("Expected:\n%#v", expected)
t.Fail()
}
})
}
}