In Chapter 11, Writing Networked Services, we saw that Go offers first-class APIs to build client and server programs using HTTP. The net/http/httptest
sub-package, part of the Go standard library, facilitates the testing automation of both HTTP server and client code, as discussed in this section.
To explore this space, we will implement a simple API service that exposes the vector operations (covered in earlier sections) as HTTP endpoints. For instance, the following source snippet partially shows the methods that make up the server (for a complete listing, see https://github.com/vladimirvivien/learning-go/ch12/service/serv.go):
package main import ( "encoding/json" "fmt" "net/http" "github.com/vladimirvivien/learning-go/ch12/vector" ) func add(resp http.ResponseWriter, req *http.Request) { var params []vector.SimpleVector if err := json.NewDecoder(req.Body).Decode(¶ms); err != nil { resp.WriteHeader(http.StatusBadRequest) fmt.Fprintf(resp, "Unable to parse request: %s ", err) return } if len(params) != 2 { resp.WriteHeader(http.StatusBadRequest) fmt.Fprintf(resp, "Expected 2 or more vectors") return } result := params[0].Add(params[1]) if err := json.NewEncoder(resp).Encode(&result); err != nil { resp.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(resp, err.Error()) return } } ... func main() { mux := http.NewServeMux() mux.HandleFunc("/vec/add", add) mux.HandleFunc("/vec/sub", sub) mux.HandleFunc("/vec/dotprod", dotProd) mux.HandleFunc("/vec/mag", mag) mux.HandleFunc("/vec/unit", unit) if err := http.ListenAndServe(":4040", mux); err != nil { fmt.Println(err) } }
golang.fyi/ch12/service/serv.go
Each function (add
, sub
, dotprod
, mag
, and unit
) implements the http.Handler
interface. The functions are used to handle HTTP requests from the client to calculate the respective operations from the vector
package. Both requests and responses are formatted using JSON for simplicity.
When writing HTTP server code, you will undoubtedly run into the need to test your code, in a robust and repeatable manner, without having to set up some fragile code harness to simulate end-to-end testing. Type httptest.ResponseRecorder
is designed specifically to provide unit testing capabilities for exercising the HTTP handler methods by inspecting state changes to the http.ResponseWriter in the tested function. For instance, the following snippet uses httptest.ResponseRecorder
to test the server's add
method:
import ( "net/http" "net/http/httptest" "strconv" "strings" "testing" "github.com/vladimirvivien/learning-go/ch12/vector" ) func TestVectorAdd(t *testing.T) { reqBody := "[[1,2],[3,4]]" req, err := http.NewRequest( "POST", "http://0.0.0.0/", strings.NewReader(reqBody)) if err != nil { t.Fatal(err) } actual := vector.New(1, 2).Add(vector.New(3, 4)) w := httptest.NewRecorder() add(w, req) if actual.String() != strings.TrimSpace(w.Body.String()) { t.Fatalf("Expecting actual %s, got %s", actual.String(), w.Body.String(), ) } }
The code uses reg, err := http.NewRequest("POST", "http://0.0.0.0/", strings.NewReader(reqBody))
to create a new *http.Request
value with a "POST"
method, a fake URL, and a request body, variable reqBody
, encoded as a JSON array. Later in the code, w := httptest.NewRecorder()
is used to create an httputil.ResponseRecorder
value, which is used to invoke the add(w, req)
function along with the created request. The value recorded in w
, during the execution of function add
, is compared with expected value stored in atual
with if actual.String() != strings.TrimSpace(w.Body.String()){...}.
Creating test code for an HTTP client is more involved, since you actually need a server running for proper testing. Luckily, package httptest
provides type httptest.Server
to programmatically create servers to test client requests and send back mock responses to the client.
To illustrate, let us consider the following code, which partially shows the implementation of an HTTP client to the vector server presented earlier (see the full code listing at https://github.com/vladimirvivien/learning-go/ch12/client/client.go). The add
method encodes the parameters vec0
and vec2
of type vector.SimpleVector
as JSON objects, which are sent to the server using c.client.Do(req)
. The response is decoded from the JSON array into type vector.SimpleVector
assigned to variable result
:
type vecClient struct { svcAddr string client *http.Client } func (c *vecClient) add( vec0, vec1 vector.SimpleVector) (vector.SimpleVector, error) { uri := c.svcAddr + "/vec/add" // encode params var body bytes.Buffer params := []vector.SimpleVector{vec0, vec1} if err := json.NewEncoder(&body).Encode(¶ms); err != nil { return []float64{}, err } req, err := http.NewRequest("POST", uri, &body) if err != nil { return []float64{}, err } // send request resp, err := c.client.Do(req) if err != nil { return []float64{}, err } defer resp.Body.Close() // handle response var result vector.SimpleVector if err := json.NewDecoder(resp.Body). Decode(&result); err != nil { return []float64{}, err } return result, nil }
golang.fyi/ch12/client/client.go
We can use type httptest.Server
to create code to test the requests sent by a client and to return data to the client code for further inspection. Function httptest.NewServer
takes a value of type http.Handler
, where the test logic for the server is encapsulated. The function then returns a new running HTTP server ready to serve on a system-selected port.
The following test function shows how to use httptest.Server
to exercise the add
method from the client code presented earlier. Notice that when creating the server, the code uses type http.HandlerFunc
, which is an adapter that takes a function value to produce an http.Handler
. This convenience allows us to skip the creation of a separate type to implement a new http.Handler
:
import( "net/http" "net/http/httptest" ... ) func TestClientAdd(t *testing.T) { server := httptest.NewServer(http.HandlerFunc( func(resp http.ResponseWriter, req *http.Request) { // test incoming request path if req.URL.Path != "/vec/add" { t.Errorf("unexpected request path %s", req.URL.Path) return } // test incoming params body, _ := ioutil.ReadAll(req.Body) params := strings.TrimSpace(string(body)) if params != "[[1,2],[3,4]]" { t.Errorf("unexpected params '%v'", params) return } // send result result := vector.New(1, 2).Add(vector.New(3, 4)) err := json.NewEncoder(resp).Encode(&result) if err != nil { t.Fatal(err) return } }, )) defer server.Close() client := newVecClient(server.URL) expected := vector.New(1, 2).Add(vector.New(3, 4)) result, err := client.add(vector.New(1, 2), vector.New(3, 4)) if err != nil { t.Fatal(err) } if !result.Eq(expected) { t.Errorf("Expecting %s, got %s", expected, result) } }
golang.fyi/ch12/client/client_test.go
The test function first sets up the server
along with its handler function. Inside the function of http.HandlerFunc
, the code first ensures that the client requests the proper path of "/vec/add"
. Next, the code inspects the request body from the client, ensuring proper JSON format and valid parameters for the add operation. Finally, the handler function encodes the expected result as JSON and sends it as a response to the client.
The code uses the system-generated server
address to create a new client
with newVecClient(server.URL)
. Method call client.add(vector.New(1, 2), vector.New(3, 4))
sends a request to the test server to calculate the vector addition of the two values in its parameter list. As shown earlier, the test server merely simulates the real server code and returns the calculated vector value. The result
is inspected against the expected
value to ensure proper working of the add
method.
3.137.216.175