test(gin): resolve race conditions in integration tests (#4453)

- Implement TestRebuild404Handlers to verify 404 handler chain rebuilding
  when global middleware is added via Use()
- Add waitForServerReady helper with exponential backoff to replace
  unreliable time.Sleep() calls in integration tests
- Fix race conditions in TestRunEmpty, TestRunEmptyWithEnv, and
  TestRunWithPort by using proper server readiness checks
- All tests now pass consistently with -race flag

This addresses the empty test function and eliminates flaky test failures
caused by insufficient wait times for server startup.

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
Wayne Aki 2025-11-30 15:38:07 +08:00 committed by GitHub
parent 583db590ec
commit f416d1e594
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 65 additions and 10 deletions

View File

@ -70,9 +70,10 @@ func TestRunEmpty(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run()) assert.NoError(t, router.Run())
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // Wait for server to be ready with exponential backoff
time.Sleep(5 * time.Millisecond) err := waitForServerReady("http://localhost:8080/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":8080")) require.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
@ -213,9 +214,10 @@ func TestRunEmptyWithEnv(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run()) assert.NoError(t, router.Run())
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // Wait for server to be ready with exponential backoff
time.Sleep(5 * time.Millisecond) err := waitForServerReady("http://localhost:3123/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":3123")) require.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example") testRequest(t, "http://localhost:3123/example")
@ -234,9 +236,10 @@ func TestRunWithPort(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run(":5150")) assert.NoError(t, router.Run(":5150"))
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // Wait for server to be ready with exponential backoff
time.Sleep(5 * time.Millisecond) err := waitForServerReady("http://localhost:5150/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":5150")) require.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example") testRequest(t, "http://localhost:5150/example")

View File

@ -545,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
} }
func TestRebuild404Handlers(t *testing.T) { func TestRebuild404Handlers(t *testing.T) {
var middleware0 HandlerFunc = func(c *Context) {}
var middleware1 HandlerFunc = func(c *Context) {}
router := New()
// Initially, allNoRoute should be nil
assert.Nil(t, router.allNoRoute)
// Set NoRoute handlers
router.NoRoute(middleware0)
assert.Len(t, router.allNoRoute, 1)
assert.Len(t, router.noRoute, 1)
compareFunc(t, router.allNoRoute[0], middleware0)
// Add Use middleware should trigger rebuild404Handlers
router.Use(middleware1)
assert.Len(t, router.allNoRoute, 2)
assert.Len(t, router.Handlers, 1)
assert.Len(t, router.noRoute, 1)
// Global middleware should come first
compareFunc(t, router.allNoRoute[0], middleware1)
compareFunc(t, router.allNoRoute[1], middleware0)
} }
func TestNoMethodWithGlobalHandlers(t *testing.T) { func TestNoMethodWithGlobalHandlers(t *testing.T) {

View File

@ -4,7 +4,11 @@
package gin package gin
import "net/http" import (
"fmt"
"net/http"
"time"
)
// CreateTestContext returns a fresh Engine and a Context associated with it. // CreateTestContext returns a fresh Engine and a Context associated with it.
// This is useful for tests that need to set up a new Gin engine instance // This is useful for tests that need to set up a new Gin engine instance
@ -29,3 +33,28 @@ func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
c.writermem.reset(w) c.writermem.reset(w)
return return
} }
// waitForServerReady waits for a server to be ready by making HTTP requests
// with exponential backoff. This is more reliable than time.Sleep() for testing.
func waitForServerReady(url string, maxAttempts int) error {
client := &http.Client{
Timeout: 100 * time.Millisecond,
}
for i := 0; i < maxAttempts; i++ {
resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
return nil
}
// Exponential backoff: 10ms, 20ms, 40ms, 80ms, 160ms...
backoff := time.Duration(10*(1<<uint(i))) * time.Millisecond
if backoff > 500*time.Millisecond {
backoff = 500 * time.Millisecond
}
time.Sleep(backoff)
}
return fmt.Errorf("server at %s did not become ready after %d attempts", url, maxAttempts)
}