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

Race condition in httptestserver when changing handler

我有一部分测试,我想 运行 它们超过 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 中的服务器 运行s 而我正在尝试同时更改处理程序。我对么?

如果我尝试使用替代代码为每个测试创建一个新服务器,则不会出现竞争:

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

所以第一个问题是,使用一个服务器进行测试片段和动态更改处理程序而无需竞争的最佳方法是什么?就性能而言,拥有一台服务器而不是创建新服务器是否值得?

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

当你想测试一个新的处理程序时,只需创建并启动一个新服务器。

如果您真的有很多处理程序想要以这种方式进行测试,并且性能对您来说至关重要,您可以创建一个 "multiplexer" 处理程序,并将其传递给单个 httptest.Server。完成处理程序的测试后,更改多路复用器处理程序的 "state" 以切换到下一个可测试的处理程序。

让我们看一个示例(将所有这些代码放入 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])
        }
    })
}

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