【问题标题】:Race condition in httptestserver when changing handler更改处理程序时 http 测试服务器中的竞争条件
【发布时间】:2018-12-25 02:19:21
【问题描述】:

我有一组测试,我想在 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()
    })
}

所以首先要问什么是使用一台服务器进行测试片并在没有比赛的情况下即时更改处理程序的最佳方式?并且在性能方面值得拥有一台服务器而不是创建新服务器吗?

【问题讨论】:

    标签: http testing go race-condition


    【解决方案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 读取它)。

    【讨论】:

    • 你运行比赛检测器吗?我在移动设备上,我自己无法做到,但对我来说,这似乎是一场数据竞赛。
    • handlerIdx 正在与服务器的另一个 goroutine 上运行。但只要只是为了这些测试,并且测试不平行,我认为就可以了。
    • @leafbebop 你是对的。运行比赛检测器,没有发现任何东西,因为测试是按顺序运行的。但是你是对的,必须同步访问handlerIdx
    • 感谢您的解释和解决方案。文档从未提到直接设置 Config.Handler 是不安全的,这有点令人困惑,更糟糕的是,测试在没有 -race 标志的情况下运行良好。我将为每次测试启动一个新服务器。
    猜你喜欢
    • 2019-04-10
    • 1970-01-01
    • 2017-07-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-04
    • 1970-01-01
    • 2014-02-23
    相关资源
    最近更新 更多