I have a slice of tests and I want to run them over one instance of httptest.Server. Each test has its own handler function.
func TestAPICaller_RunApiMethod(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(nil))
defer server.Close()
for _, test := range testData {
server.Config.Handler = http.HandlerFunc(test.handler)
t.Run(test.Name, func(t *testing.T) {
... some code which calls server
}
})
}
This code gives a race when running with "go test -race". It is probably because server runs in goroutine and I'm trying to change a handler concurrently. Am I correct?
If I try alternative code where I create a new server for every test, then no races:
func TestAPICaller_RunApiMethod(t *testing.T) {
for _, test := range testData {
server := httptest.NewServer(http.HandlerFunc(test.handler))
t.Run(test.Name, func(t *testing.T) {
... some code which calls server
}
server.Close()
})
}
So first question what is the best way to use one server for a slice of tests and change handler on the fly without races? And is it worth it in terms of performance to have one server instead of creating new ones?
httptest.Server
was not "designed" to change its handler. You may only change its handler if you've created it with httptest.NewUnstartedServer()
, and only before you start it with Server.Start()
or Server.StartTLS()
.
Just create and start a new server when you want to test a new handler.
If you have really a lot of handlers you want to test this way and performance is that critical for you, you may create a "multiplexer" handler, and pass that to a single httptest.Server
. When you're done testing a handler, change the "state" of the multiplexer handler to switch over to the next testable handler.
Let's see an example how it could look like (put all these code into TestAPICaller_RunApiMethod
):
Let's say we want to test the following handlers:
handlersToTest := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte{0}) }),
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte{1}) }),
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte{2}) }),
}
Here's an example multiplexer handler:
handlerIdx := int32(0)
muxHandler := func(w http.ResponseWriter, r *http.Request) {
idx := atomic.LoadInt32(&handlerIdx)
handlersToTest[idx].ServeHTTP(w, r)
}
Which we use for the test server:
server := httptest.NewServer(http.HandlerFunc(muxHandler))
defer server.Close()
And code to test all handlers:
for i := range handlersToTest {
atomic.StoreInt32(&handlerIdx, int32(i))
t.Run(fmt.Sprint("Testing idx", i), func(t *testing.T) {
res, err := http.Get(server.URL)
if err != nil {
log.Fatal(err)
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
res.Body.Close()
if len(data) != 1 || data[0] != byte(i) {
t.Errorf("Expected response %d, got %d", i, data[0])
}
})
}
One thing to note here: the "state" of the multiplexer handler is the handlerIdx
variable. Since the multiplexer handler is called from another goroutine, access to this variable must be synchronized (because we're writing to it and the server's goroutine reads it).