Search code examples
gotestinggo-testing

How to simulate multiple different HTTP responses using Go's httptest?


I have created some Go functions that make HTTP GET calls to services that are out there on the internet and parse the results.

I am now working on writing test-cases for these functions. In my test cases, I'm using the go package httptest to simulate calls to these external services. Below is my code. Error checking is purposefully removed for brevity. Here is the go-playground.

package main

import (
    "fmt"
    "io"
    "context"
    "net/http"
    "net/http/httptest"
)

func handlerResponse() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"A":"B"}`))
    })
}

func buildMyRequest(ctx context.Context, url string) *http.Request {
    request, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    return request
}

func myPrint(response *http.Response) {
    b := make([]byte, 60000)
    for {
        _, err := response.Body.Read(b)
        if err == io.EOF {
            break
        }
    }
    fmt.Println(string(b))
}

func main() {
    srv := httptest.NewServer(handlerResponse())            
    client := http.Client{}

    myResponse1, _ := client.Do(buildMyRequest(context.Background(), srv.URL))
    fmt.Println("myResponse1:")
    myPrint(myResponse1)
    
    myResponse2, _ := client.Do(buildMyRequest(context.Background(), srv.URL))
    fmt.Println("myResponse2:")
    myPrint(myResponse2)

}

This is the output it produces:

myResponse1:
{"A":"B"}
myResponse2:
{"A":"B"}

As you can see, I have created some dummy HTTP response data {"A":"B"} and when you send an HTTP request to srv.URL, it actually hits an ephemeral HTTP server which responds with the dummy data. Cool!

When you send the second HTTP request to srv.URL, it again responds with the same dummy data. But this is where my problem arises. I want the ephemeral HTTP server to return some different data the second time {"C":"D"} and third time {"E":"F"} it receives a request.

How can I change the first line of the main() function so that the server responds with my desired data on subsequent HTTP calls?


Solution

  • you could use a hack like follows ( playground : here)

    package main
    
    import (
        "fmt"
        "io"
        "context"
        "net/http"
        "net/http/httptest"
        "sync"
    )
    
    
    type responseWriter struct{
       resp map[int]string
       count int
       lock *sync.Mutex
    }
    
    func NewResponseWriter()*responseWriter{
       r := new(responseWriter)
       r.lock = new(sync.Mutex)
       r.resp = map[int]string{
        0: `{"E":"F"}`,
        1: `{"A":"B"}`,
        2: `{"C":"D"}`,
       }
       r.count = 0
       return r
    }
    
    func (r *responseWriter)GetResp()string{
       r.lock.Lock()
       defer r.lock.Unlock()
       r.count ++
       return r.resp[r.count%3]
    }
    
    
    func handlerResponse(rr *responseWriter) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(http.StatusOK)
            w.Write([]byte(rr.GetResp()))
        })
    }
    
    func buildMyRequest(ctx context.Context, url string) *http.Request {
        request, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
        return request
    }
    
    
    func myPrint(response *http.Response) {
        b := make([]byte, 60000)
        for {
            _, err := response.Body.Read(b)
            if err == io.EOF {
                break
            }
        }
        fmt.Println(string(b))
    }
    
    func main() {
            rr := NewResponseWriter()
    
        srv := httptest.NewServer(handlerResponse(rr))  
        client := http.Client{}
    
        myResponse1, err := client.Do(buildMyRequest(context.Background(), srv.URL))
        if err != nil{
           fmt.Println(err)
           return
        }
        
        defer myResponse1.Body.Close()
        fmt.Println("myResponse1:")
        myPrint(myResponse1)
        
        myResponse2, err := client.Do(buildMyRequest(context.Background(), srv.URL))
        if err != nil{
           fmt.Println(err)
           return
        }
        
        defer myResponse2.Body.Close()
        fmt.Println("myResponse2:")
        myPrint(myResponse2)
    }