HTTP testing

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(&params);  
       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.

Testing HTTP server code

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()){...}.

Testing HTTP client code

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(&params); 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.216.175