更改处理程序时,httptestserver中的竞争条件

时间:2018-07-17 11:35:05

标签: http testing go race-condition

我有一部分测试,我想在httptest.Server的一个实例上运行它们。每个测试都有其自己的处理函数。

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
        }
    })
}

使用“ go test -race”运行时,此代码可引起竞赛。可能是因为服务器在goroutine中运行,并且我试图同时更改处理程序。我对么?

如果我尝试在替代代码中为每个测试创建一个新服务器,则不会出现种族:

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

那么第一个问题是,使用一个服务器进行测试并在不产生竞争的情况下即时更改处理程序的最佳方法是什么?就性能而言,拥有一台服务器而不是创建新服务器是否值得?

1 个答案:

答案 0 :(得分:1)

httptest.Server并非“设计”用来更改其处理程序。仅当您使用httptest.NewUnstartedServer()创建了它的处理程序后,才可以使用Server.Start()Server.StartTLS()来更改它的处理程序。

要测试新处理程序时,只需创建并启动新服务器即可。

如果您真的有很多处理程序想要通过这种方式进行测试,而性能对您而言至关重要,则可以创建一个“多路复用器”处理程序,并将其传递给单个httptest.Server。测试完处理程序后,更改多路复用器处理程序的“状态”以切换到下一个可测试的处理程序。

让我们看一个示例,其外观如何(将所有这些代码放入TestAPICaller_RunApiMethod中):

假设我们要测试以下处理程序:

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}) }),
}

这是一个示例多路复用器处理程序:

handlerIdx := int32(0)
muxHandler := func(w http.ResponseWriter, r *http.Request) {
    idx := atomic.LoadInt32(&handlerIdx)
    handlersToTest[idx].ServeHTTP(w, r)
}

我们用于测试服务器的

server := httptest.NewServer(http.HandlerFunc(muxHandler))
defer server.Close()

和测试所有处理程序的代码:

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])
        }
    })
}

这里要注意的一件事:多路复用器处理程序的“状态”是handlerIdx变量。由于多路复用处理程序是从另一个goroutine调用的,因此必须同步访问此变量(因为我们正在对其进行写入,并且服务器的goroutine会读取它)。